Fixing Pylance 'Expected 0 Positional Arguments' with Classic attrs and Python 3.5
Understanding the Pylance and attrs Type Error in Python 3.5
When working with legacy Python environments like Python 3.5, developers frequently use the attrs library as a backward-compatible alternative to dataclasses. However, because Python 3.5 lacks native variable type annotations (PEP 526), developers must rely on PEP 484 comment-style annotations:
import attr
from typing import List
@attr.s
class Foo:
names = attr.ib() # type: List[str]
Foo(['James'])
When analyzing this code, modern versions of Pylance (backed by Microsoft's Pyright engine) often flag the instantiation with the error:
Expected 0 positional arguments
# or
No parameter named "names"
Despite the code running completely fine at runtime, static type checking fails.
Why Does Pylance Ignore the Generated __init__ Method?
Pylance includes built-in semantic analyzers that synthesize the generated __init__ methods for libraries like attrs and dataclasses. However, modern static type checkers have largely phased out full synthesis support for Python 3.5 comment-based annotations inside class-level attribute assignments.
Specifically, Pyright's attrs transform parser expects PEP 526 annotations (such as names: List[str] = attr.ib()) to infer field presence and dynamically build the signature for __init__. When only type comments or attr.ib(type=...) are provided, Pylance falls back to the default empty class constructor, triggering the "Expected 0 positional arguments" error.
Solutions and Workarounds
1. Use Interface Stub Files (.pyi) [Recommended for Legacy Codebases]
If you cannot upgrade your Python runtime and want to keep your implementation files clean without boilerplate TYPE_CHECKING guards, create a companion stub file (.pyi). Type checkers prioritize .pyi files for interface validation:
Create a file named foo.pyi in the same directory as foo.py:
# foo.pyi
from typing import List
import attr
@attr.s
class Foo:
names: List[str]
def __init__(self, names: List[str]) -> None: ...
This decouples modern type checking annotations from your runtime code, keeping Python 3.5 execution intact while giving Pylance the exact signature it expects.
2. The typing.TYPE_CHECKING Constructor Guard
If you prefer to keep everything in a single file, you can explicitly define the __init__ method under a TYPE_CHECKING conditional guard. Because TYPE_CHECKING evaluates to False at runtime, this will not interfere with attrs runtime generation:
import attr
from typing import List, TYPE_CHECKING
@attr.s
class Foo:
names = attr.ib() # type: List[str]
if TYPE_CHECKING:
def __init__(self, names: List[str]) -> None:
self.names = names
Foo(['James']) # No errors flagged
3. Configure Pyright Settings in VS Code
Ensure that VS Code is explicitly configured to parse your target Python version. In your pyrightconfig.json or VS Code settings.json, set the target version:
{
"python.analysis.typeCheckingMode": "basic",
"pyright.pythonVersion": "3.5"
}
Note: Because Pyright officially dropped support for Python 3.5 targets, newer Pylance versions might ignore legacy syntax quirks. If you strictly require automated type synthesis for PEP 484 comment-based attrs, you may need to pin to a legacy version of the Pylance extension.
4. Long-Term: Migrate to Python 3.6+
Python 3.5 reached end-of-life (EOL) in September 2020. If project constraints allow, upgrading to Python 3.6 or newer unlocks standard variable annotations:
import attr
from typing import List
@attr.s(auto_attribs=True)
class Foo:
names: List[str]
Foo(['James'])
This syntax is natively recognized by modern Pylance/Pyright, Mypy, and IDEs without requiring any additional boilerplate.