py-types
Skill1 starsChanged 27 days agoMITView on GitHub
Python type system advisor. Always use this skill when working with Python type hints, mypy, Protocol, TypedDict, dataclasses, or generics. Use when the user asks "how do I type this in Python", "mypy is complaining", "should I use TypedDict or dataclass", "how do I type a dict", "what's Optional", "how do I make a generic class", "Protocol vs ABC vs TypedDict", "how do I annotate a function that returns X or Y", or "how do I avoid Any". Read this skill before answering any Python type annotation question.
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 0 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 42
Use Any only at true integration boundaries (untyped third-party libs) and isolate it with a narrow wrapper.
- Very specific: line 46
Always annotate None explicitly.
- Very specific: line 89
Don't use plain dict[str, Any] for structured data: it hides the shape and loses all checking.
- Very specific: line 109
Prefer Protocol over ABC when the type relationship is structural rather than nominal.
- Very specific: line 113
Use TypeVar to write functions that preserve type information:
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Python Types
Python's type system is gradual: you can add hints incrementally without breaking anything. But partial typing is a trap: unannotated code is treated as Any by mypy, and Any propagates silently. The goal is enough coverage that mypy can catch real mistakes.
Start with --strict in mypy
The default mypy configuration is lenient. Enable strict mode to get meaningful coverage:
# mypy.ini or pyproject.toml [tool.mypy]
strict = trueOr per-file to adopt gradually:
# mypy: strictKey strict flags: --disallow-untyped-defs, --disallow-any-generics, --warn-return-any. Fix these first. They catch the most real bugs.
Any vs object
Any is a two-way escape hatch: it's compatible with every type and every type is compatible with it. This means errors hide.
# Bad: Any propagates — x.anything() won't be caught
def process(x: Any) -> Any:
return x.value
# Better: object is the base type — forces you to narrow before using
def process(x: object) -> str:
if isinstance(x, MyModel):
return x.value
raise TypeError(f"Unexpected type: {type(x)}")Use Any only at true integration boundaries (untyped third-party libs) and isolate it with a narrow wrapper.
Nullable types
Always annotate None explicitly. A missing annotation and Optional[X] are not the same; the latter is checked.
# Python 3.10+: use the union shorthand
def find_user(id: str) -> User | None: ...
# Earlier versions: use Optional
from typing import Optional
def find_user(id: str) -> Optional[User]: ...
# Bad: return type omitted — mypy treats return as Any
def find_user(id: str):
...TypedDict vs dataclass vs NamedTuple vs Pydantic
| Use | When |
TypedDict | Typing existing dict-shaped data (JSON responses, config dicts) |
dataclass | Mutable structured data with methods; Python-native |
NamedTuple | Immutable records; tuple-compatible |
Pydantic BaseModel | External data with validation (API input, config files) |
# TypedDict — for dict shapes you don't control
from typing import TypedDict
class UserDict(TypedDict):
id: str
name: str
email: str | None
# dataclass — for your own structured data
from dataclasses import dataclass, field
@dataclass
class User:
id: str
name: str
tags: list[str] = field(default_factory=list)Don't use plain dict[str, Any] for structured data: it hides the shape and loses all checking.
Protocol for structural subtyping
Python uses duck typing. Protocol lets you express "any object with these methods" without requiring inheritance.
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
def shutdown(resource: Closeable) -> None:
resource.close()
# Works for any class that has .close() — no inheritance required
shutdown(open('file.txt'))
shutdown(db_connection)Prefer Protocol over ABC when the type relationship is structural rather than nominal. It's more Pythonic and doesn't require modifying the implementing class.
Generics
Use TypeVar to write functions that preserve type information:
from typing import TypeVar
T = TypeVar('T')
def first(items: list[T]) -> T | None:
return items[0] if items else None
# Return type is inferred correctly:
result: str | None = first(['a', 'b']) # T = strConstrain TypeVar when you need to access members:
from typing import TypeVar
from typing import Protocol
class HasId(Protocol):
id: str
T = TypeVar('T', bound=HasId)
def get_id(item: T) -> str:
return item.idPython 3.12+ supports the new type statement and [T] syntax for generics; use it for new code targeting 3.12+.
Literal and Final
Literal constrains a value to specific options:
from typing import Literal
Direction = Literal['north', 'south', 'east', 'west']
def move(direction: Direction) -> None: ...
move('north') # ok
move('up') # mypy errorFinal marks a name as a constant:
from typing import Final
MAX_RETRIES: Final = 3
MAX_RETRIES = 5 # mypy errorAvoiding circular imports with TYPE_CHECKING
Type annotations can cause circular imports. Use the TYPE_CHECKING guard to import only at type-check time:
from __future__ import annotations # enables postponed evaluation of annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import User # only imported during type checking, not at runtime
def process(user: User) -> None: # string annotation resolved by mypy
...What to watch for in code review
- Functions with no return type annotation: mypy skips them entirely
dictorlistused without type parameters (dict[str, Any]vsdict)Optional[X]return types whereNoneis never actually returned (misleads callers)Anyin return types: errors from the returned value won't be caught downstream# type: ignorecomments without an explanation of why
In this repository25 other files it gives its agents.
- api-designSkill
- arch-reviewSkill
- db-reviewSkill
- debugSkill
- dotnet-asyncSkill
- dotnet-opsSkill
- dotnet-testingSkill
- dotnet-typesSkill
- go-concurrencySkill
- go-opsSkill
- go-testingSkill
- go-typesSkill
- incidentSkill
- node-asyncSkill
- node-opsSkill
- perfSkill
- pr-prepSkill
- py-asyncSkill
- py-opsSkill
- py-testingSkill
- refactorSkill
- securitySkill
- test-genSkill
- ts-testingSkill
- ts-typesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

