diff --git a/CHANGELOG.md b/CHANGELOG.md
index c00e2c0a..cb92b7cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Features:
 * introduce Y033 (always use annotations in stubs, rather than type comments).
 * introduce Y034 (detect common errors where return types are hardcoded, but they
   should use `TypeVar`s instead).
+* introduce Y035 (`__all__` in a stub has the same semantics as at runtime).
 
 ## 22.1.0
 
diff --git a/README.md b/README.md
index a1013383..02df231e 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ currently emitted:
 | Y032 | The second argument of an `__eq__` or `__ne__` method should usually be annotated with `object` rather than `Any`.
 | Y033 | Do not use type comments (e.g. `x = ... # type: int`) in stubs, even if the stub supports Python 2. Always use annotations instead (e.g. `x: int`).
 | Y034 | Y034 detects common errors where certain methods are annotated as having a fixed return type, despite returning `self` at runtime. Such methods should be annotated with `_typeshed.Self`.<br><br>This check looks for `__new__`, `__enter__` and `__aenter__` methods that return the class's name unparameterised. It also looks for `__iter__` methods that return `Iterator`, even if the class inherits directly from `Iterator`, and for `__aiter__` methods that return `AsyncIterator`, even if the class inherits directly from `AsyncIterator`. The check excludes methods decorated with `@overload` or `@abstractmethod`.
+| Y035 | `__all__` in a stub file should always have a value, as `__all__` in a `.pyi` file has identical semantics to `__all__` in a `.py` file. E.g. write `__all__ = ["foo", "bar"]` instead of `__all__: list[str]`.
 
 Many error codes enforce modern conventions, and some cannot yet be used in
 all cases:
diff --git a/pyi.py b/pyi.py
index df1ab41d..eec09464 100644
--- a/pyi.py
+++ b/pyi.py
@@ -660,6 +660,12 @@ def visit_Expr(self, node: ast.Expr) -> None:
             self.generic_visit(node)
 
     def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
+        if _is_name(node.target, "__all__") and not self.in_class.active:
+            with self.string_literals_allowed.enabled():
+                self.generic_visit(node)
+            if node.value is None:
+                self.error(node, Y035)
+            return
         self.generic_visit(node)
         if _is_TypeAlias(node.annotation):
             return
@@ -1258,3 +1264,4 @@ def parse_options(cls, optmanager, options, extra_args) -> None:
 )
 Y033 = 'Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int")'
 Y034 = 'Y034 {methods} usually return "self" at runtime. Consider using "_typeshed.Self" in "{method_name}", e.g. "{suggested_syntax}"'
+Y035 = 'Y035 "__all__" in a stub file must have a value, as it has the same semantics as "__all__" at runtime.'
diff --git a/tests/__all__.pyi b/tests/__all__.pyi
new file mode 100644
index 00000000..a20a0162
--- /dev/null
+++ b/tests/__all__.pyi
@@ -0,0 +1,7 @@
+__all__: list[str]  # Y035 "__all__" in a stub file must have a value, as it has the same semantics as "__all__" at runtime.
+__all__: list[str] = ["foo", "bar", "baz"]
+__all__ = ["foo", "bar", "baz"]
+
+foo: int = ...
+bar: str = ...
+baz: list[set[bytes]] = ...