| name | pyright |
| description | [Applies to: **/*.py] This guide provides definitive, actionable best practices for configuring and using pyright (and basedpyright) to ensure robust, type-safe Python codebases in 2025. |
| source | cursor_mdc |
pyright Best Practices
pyright is the definitive static type-checker for modern Python, essential for "type-first" development. We standardize on basedpyright for its stricter defaults and enhanced diagnostics, ensuring our codebase remains consistently type-clean.
1. Project-Wide Configuration
Always define a pyrightconfig.json (or [tool.pyright] in pyproject.toml) at your project root. Commit this file to version control to guarantee consistent type-checking across all developer environments and CI pipelines. Start with a baseline, fix errors, then progressively tighten rules.
❌ BAD: No config file, relying on default pyright behavior.
✅ GOOD: Explicitly define your type-checking rules.
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__"],
"reportMissingTypeStubs": "error",
"reportAny": "error",
"reportExplicitAny": "error",
"reportImplicitRelativeImport": "error",
"reportInvalidCast": "error",
"reportUnsafeMultipleInheritance": "error",
"strict": [
"src/core",
"src/models"
],
"stubPath": "stubs"
}
2. Strict Diagnostics & Any Usage
Embrace strict diagnostics to eliminate implicit Any types and enforce explicit type declarations. basedpyright's reportAny and reportExplicitAny are non-negotiable for maintaining a truly type-safe codebase.
Prefer object over Any
Use object when a function truly accepts any Python object, but its type isn't relevant to its operation (e.g., for str(), repr(), or passing to a callback that ignores its argument). Reserve Any only when the type system cannot express the intent.
❌ BAD: Overusing Any, disabling type checks.
from typing import Any
def process_data(data: Any) -> Any:
print(str(data))
return data
✅ GOOD: Using object for truly generic inputs, Any only when necessary.
from typing import Any, Callable
def process_data(data: object) -> object:
print(str(data))
return data
def call_callback(cb: Callable[[int], object]) -> None:
cb(42)
def dynamic_factory(name: str) -> Any:
if name == "int": return 1
if name == "str": return "hello"
return None
Explicit Error Codes for Ignores
Always specify the exact pyright error code when suppressing diagnostics. This prevents accidentally ignoring other, unrelated errors on the same line and improves maintainability. basedpyright's reportIgnoreCommentWithoutRule enforces this.
❌ BAD: Generic type ignores.
import os
path = os.getenv("MY_PATH")
✅ GOOD: Specific pyright ignore comments.
import os
from typing import cast
path = cast(str, os.getenv("MY_PATH"))
3. Type Aliases
Declare type aliases using TypeAlias for clarity and consistency. This makes the intent explicit and helps pyright understand your type definitions.
❌ BAD: Implicit type aliases.
MyDict = dict[str, int]
✅ GOOD: Explicit TypeAlias declaration.
from typing import TypeAlias
MyDict: TypeAlias = dict[str, int]
4. Arguments and Return Types
Function Arguments: Prefer Protocols and Abstract Types
For function arguments, use abstract base classes (ABCs) or protocols (Iterable, Sequence, Mapping) to maximize flexibility and reusability. This allows your functions to accept a wider range of compatible types.
❌ BAD: Overly specific argument types.
def process_items(items: list[str]) -> None:
for item in items:
print(item)
✅ GOOD: Using abstract types for arguments.
from collections.abc import Iterable
def process_items(items: Iterable[str]) -> None:
for item in items:
print(item)
Function Return Types: Prefer Concrete Types
For return values, prefer concrete types (list, dict) unless you are returning an instance of a protocol or an abstract base class where the specific implementation is not relevant to the caller. Avoid union return types that force isinstance() checks.
❌ BAD: Abstract return types for concrete implementations, or complex unions.
from collections.abc import MutableMapping
from typing import Union
def create_mapping() -> MutableMapping[str, int]:
return {"a": 1, "b": 2}
def get_value(key: str) -> Union[str, int, None]:
if key == "name": return "Alice"
if key == "age": return 30
return None
✅ GOOD: Concrete return types, simpler unions when necessary.
def create_mapping() -> dict[str, int]:
return {"a": 1, "b": 2}
def get_value(key: str) -> str | int | None:
if key == "name": return "Alice"
if key == "age": return 30
return None
5. Modern Type Hint Syntax
Always use the modern shorthand syntax for unions (X | Y) and built-in generics (list[int]). Place None as the last element in a union.
❌ BAD: Legacy typing module syntax.
from typing import Union, Optional, List, Dict, Type
def foo(x: Union[str, int]) -> None: ...
def bar(x: Optional[str]) -> Optional[int]: ...
def baz(items: List[str]) -> Dict[str, Type[object]]: ...
✅ GOOD: Modern, concise type hint syntax.
def foo(x: str | int) -> None: ...
def bar(x: str | None) -> int | None: ...
def baz(items: list[str]) -> dict[str, type[object]]: ...
6. Type Stubs Management
Maintain a stubs/ directory for custom type stubs for libraries that lack them. Configure pyright to use this path and enable reportMissingTypeStubs. Keep third-party libraries updated to leverage inline types.
{
"stubPath": "stubs",
"reportMissingTypeStubs": "error"
}
7. Virtual Environments
Always use virtual environments. Configure pyright to correctly locate your environment to ensure it uses the installed package types.
{
"venvPath": ".venv",
"venv": "my_project_env"
}
8. Packaging for Type Safety
For libraries, include a py.typed marker file in your package to signal that it provides type information. This allows downstream users to benefit from your type annotations.
# my_library/py.typed
# This file can be empty. Its presence indicates the package is type-annotated.
9. Testing Approaches & CI Integration
Integrate pyright into your CI pipeline and pre-commit hooks. This ensures that type errors are caught early, preventing them from reaching the main branch.
- repo: https://github.com/DetachHead/basedpyright
rev: v1.1.407
hooks:
- id: basedpyright
args: ["--ignoreexternal"]
10. Common Pitfalls & basedpyright Gotchas
basedpyright introduces critical rules to prevent common typing mistakes:
reportImplicitRelativeImport
Bans ambiguous relative imports that work as scripts but fail as modules.
❌ BAD: Implicit relative import.
def func_a(): pass
import module_a
✅ GOOD: Explicit relative import.
from . import module_a
reportInvalidCast
Prevents casting to types that have no overlap with the original, indicating a logical error.
❌ BAD: Invalid cast.
from typing import cast
value: int = 10
str_value = cast(str, value)
✅ GOOD: Valid cast (e.g., narrowing a union).
from typing import cast
value: int | None = None
if value is not None:
int_value = cast(int, value)
reportUnsafeMultipleInheritance
Discourages multiple inheritance when base classes have __init__ or __new__ methods, as it's often unsafe and unpredictable.
❌ BAD: Unsafe multiple inheritance.
class BaseA:
def __init__(self): super().__init__()
class BaseB:
def __init__(self): pass
class MyClass(BaseA, BaseB):
pass
✅ GOOD: Avoid unsafe multiple inheritance or use mixins carefully.
class BaseA:
def __init__(self): pass
class MyMixin:
def do_something(self): pass
class MyClass(BaseA, MyMixin):
def __init__(self):
super().__init__()