"Readability counts." — The Zen of Python
This document defines the Python coding standards enforced by this agent. It is opinionated by design. These are not suggestions — they are the baseline expectations for any Python code this agent reviews, refactors, or produces.
The standards draw from three authoritative sources:
- The Python Language Reference
- PEP 8 — Style Guide for Python Code
- Fluent Python by Luciano Ramalho
The Zen of Python is not decoration. It is the decision framework when style questions arise. The most operationally important koans are:
- Explicit is better than implicit. Never rely on side effects, magic globals, or implicit type coercion when an explicit alternative exists.
- Simple is better than complex. Complex is better than complicated. Choose
the simplest construct that fully expresses the intent. A list comprehension
is simpler than a manual loop with
.append(). A generator expression is simpler than building a full list you only iterate once. - Flat is better than nested. If a function has more than two levels of indentation, it likely needs extraction or early returns.
- There should be one — and preferably only one — obvious way to do it.
When the standard library provides a tool, use it. Do not reinvent
itertools,functools,collections, orpathlib. - If the implementation is hard to explain, it's a bad idea. Code that requires a paragraph of comments to justify its structure should be restructured, not annotated.
- Namespaces are one honking great idea. Use modules and packages to
organize code. Avoid polluting the global namespace. Never use wildcard
imports (
from module import *).
Naming is not cosmetic. Names are the primary documentation layer.
- Variables and functions:
snake_case. No exceptions. - Classes:
PascalCase. No exceptions. - Constants:
UPPER_SNAKE_CASE. Defined at module level. - Private attributes/methods: Single leading underscore (
_internal). This is a convention, not enforcement — but the agent treats violations as intentional API exposure. - Name mangling: Double leading underscore (
__mangled) is reserved for avoiding name collisions in inheritance hierarchies. Do not use it as a "more private" marker. - Dunder methods:
__init__,__repr__, etc. Never invent custom dunder names. The dunder namespace belongs to the language.
- Booleans should read as predicates:
is_valid,has_permission,should_retry. Nevervalid,permission,retryfor boolean values. - Functions should be verbs or verb phrases:
calculate_total,parse_response,validate_input. If a function returns a boolean, it should read as a question:is_expired(),has_children(). - Variables should be nouns or noun phrases that describe what they hold,
not how they were computed:
user_countnotlen_result. - Avoid abbreviations unless they are universally understood in context
(
url,http,db,config). Neverusr,mgr,ctxunless the domain demands it (e.g.,ctxin click/typer CLIs is idiomatic). - Avoid generic names:
data,info,result,temp,val,item. These are placeholders, not names. Every variable should answer: "what is this?"
- Short, lowercase, no underscores if possible:
utils,models,config. - If a module name requires an underscore, it may indicate the module is doing too much and should be split.
Python is a dynamically typed language that supports optional static typing. This agent treats type annotations as mandatory, not optional.
- All function signatures must be fully annotated — parameters and return types. No exceptions.
- Use modern syntax (Python 3.10+):
X | Noneinstead ofOptional[X]list[str]instead ofList[str]dict[str, int]instead ofDict[str, int]tuple[int, ...]instead ofTuple[int, ...]
- Use
Self(fromtyping) for methods that return their own class. - Avoid
Anyunless interfacing with genuinely untyped external code, and document why. - Use
TypeAliasfor complex type expressions that appear more than once:type UserId = int type UserMap = dict[UserId, User]
- Use
Protocolover abstract base classes when you only need structural subtyping (duck typing with type safety). - Use
TypeVarandGenericcorrectly for container types and polymorphic functions. Prefer the new[T]syntax (PEP 695) where supported.
- Local variables where the type is obvious from assignment:
# Unnecessary — the type is obvious name: str = "Alice" # Useful — the type clarifies intent connections: dict[str, list[Connection]] = {}
selfandcls— these are implicit and should not be annotated.
Imports appear at the top of the file, organized into three groups separated by blank lines (per PEP 8):
- Standard library imports
- Third-party imports
- Local/project imports
Within each group, imports are sorted alphabetically. Use ruff (specifically
isort-compatible rules) to enforce this automatically.
- Absolute imports only. Relative imports (
from . import x) are acceptable only within a package's internal modules and should be used sparingly. - Never use wildcard imports (
from x import *). They pollute the namespace and make dependency tracking impossible. - Import modules, not objects, when the module name adds clarity:
# Prefer this when the module name adds context import os.path # Prefer this when the object name is self-explanatory from pathlib import Path from collections import defaultdict
- Do not import from inside functions unless there is a genuine reason (circular import resolution, optional heavy dependency). Document every exception with a comment explaining why.
- One import per line for
fromimports with more than three names:from module import ( ClassA, ClassB, function_c, )
- Single responsibility. A function does one thing. If the name requires "and" to describe it, split it.
- Small surface area. Fewer parameters is better. More than three positional
parameters is a code smell. Use keyword-only arguments (
*) or a config object (Pydantic model) for complex signatures. - Pure functions where possible. Functions that take inputs and return outputs without side effects are easier to test, compose, and reason about.
- Early returns over deep nesting. Guard clauses at the top, happy path at
the bottom:
def process(item: Item) -> Result: if not item.is_valid: raise InvalidItemError(item.id) if item.is_cached: return item.cached_result return _compute_result(item)
- Use keyword-only arguments for any parameter that isn't self-evident from
position:
def connect(host: str, port: int, *, timeout: float = 30.0, retries: int = 3) -> Connection: ...
- Use positional-only parameters (
/) for functions where parameter names are implementation details:def distance(x1: float, y1: float, x2: float, y2: float, /) -> float: ...
- Never use mutable default arguments. Use
Noneand initialize inside:def process(items: list[str] | None = None) -> list[str]: items = items or [] ...
- All public functions, classes, and modules must have docstrings.
- Use Google-style docstrings for consistency:
def calculate_score(responses: list[Response], *, weights: dict[str, float] | None = None) -> float: """Calculate a weighted score from survey responses. Applies the provided weights to each response category. If no weights are given, all categories are weighted equally. Args: responses: Survey response objects to score. weights: Optional mapping of category names to weight multipliers. Returns: The calculated score as a float between 0.0 and 1.0. Raises: ValueError: If responses is empty. KeyError: If weights reference a category not in responses. """
- Docstrings describe what and why, not how. The code shows how.
- When you need state + behavior together. If a class has no methods beyond
__init__, it should probably be adataclass,NamedTuple, or Pydantic model. - When you need to implement a protocol or interface.
- When identity matters — i.e., two instances with the same data are not interchangeable.
- Bags of functions — if a class is just a namespace for static methods, use a module instead.
- Single-method classes — if a class has only
__init__and one other method, it should be a function (possibly a closure). - Data containers without behavior — use Pydantic
BaseModel,dataclass, orNamedTuple.
- Always define
__repr__for debuggability.__str__is optional and should only differ from__repr__when a human-friendly format is needed. - Use
__slots__on classes that will be instantiated frequently, unless you need dynamic attribute assignment. - Prefer composition over inheritance. Inheritance creates coupling. Composition via protocols and dependency injection creates flexibility.
- Limit inheritance depth to two levels (base → concrete). If you need more, you need a different design.
- Use
@classmethodfor alternative constructors (from_json,from_config, etc.). - Use
@staticmethodsparingly — if a method doesn't use the class or instance, ask whether it belongs on the class at all. - Use
@propertyfor computed attributes that should look like attribute access. Never use it for expensive operations — the caller has no signal that a property is costly.
| Need | Use |
|---|---|
| Immutable record with few fields | NamedTuple |
| Mutable record with defaults | dataclass |
| Validation, serialization, config | Pydantic BaseModel |
| API request/response models | Pydantic BaseModel |
| Simple enum of choices | enum.Enum or enum.StrEnum |
| Type-safe dictionary | TypedDict |
- Never use raw dictionaries as primary data structures for domain objects. Dictionaries are for truly dynamic key-value data. Known-shape data gets a model.
- Prefer immutability. Use
frozen=Trueon dataclasses or Pydantic'smodel_config = ConfigDict(frozen=True)unless mutation is required. - Use
StrEnumfor string-valued enums (Python 3.11+). They serialize naturally and work in match statements.
- Be specific. Catch specific exceptions, never bare
except:orexcept Exception:unless you are at a top-level boundary (CLI entry point, request handler) where you log and re-raise or translate. - Fail fast. Validate inputs at the boundary. Do not let invalid data propagate through layers.
- Use custom exceptions for domain-specific error conditions. Group them in
a dedicated
exceptions.pymodule:class AppError(Exception): """Base exception for the application.""" class ValidationError(AppError): """Raised when input validation fails.""" class NotFoundError(AppError): """Raised when a requested resource does not exist."""
- Never silence exceptions without logging:
# Never try: do_something() except SomeError: pass # Acceptable, with justification try: do_something() except SomeError: logger.info("Ignoring expected SomeError during cleanup")
- Use
contextlib.suppressfor intentionally ignored exceptions (one-liners only):with suppress(FileNotFoundError): path.unlink()
- Write custom context managers for resource management (
__enter__/__exit__or@contextmanager).
- Use comprehensions when they are clearer than the loop equivalent. If a comprehension exceeds one line comfortably, use a loop or extract a function.
- Never nest more than two levels of comprehension.
- Use dict comprehensions over
dict(zip(...))when building dictionaries from parallel iterables.
- Use generator expressions (
(x for x in ...)) when you only need to iterate once and don't need the full list in memory. - Use
yield-based generators for complex lazy sequences. - Use
itertoolsbefore writing custom iteration logic:chain,islice,groupby,product,combinations,starmap.
- Use tuple unpacking in loops:
for name, score in results.items(): ...
- Use
enumerateinstead of manual index tracking. - Use
zip(withstrict=Trueon Python 3.10+) for parallel iteration. - Use the walrus operator (
:=) when it eliminates redundant computation in conditions, but not when it harms readability.
- Use f-strings for all string interpolation. No
.format(), no%formatting. - Use triple-quoted strings for multi-line content. Avoid string
concatenation with
+across lines. - Use
pathlib.Pathfor all filesystem path construction. Never use string concatenation oros.path.join.
| Workload | Use |
|---|---|
| I/O-bound (HTTP, file, DB) | asyncio |
| CPU-bound (computation) | multiprocessing or concurrent.futures.ProcessPoolExecutor |
| Simple parallelism | concurrent.futures.ThreadPoolExecutor (I/O) or ProcessPoolExecutor (CPU) |
- If the project uses async, go fully async. Do not mix sync and async
I/O in the same call chain. Use
asyncio.to_threadfor unavoidable sync calls from async contexts. - Use
async withandasync forfor async context managers and iterators. - Use
asyncio.TaskGroup(Python 3.11+) overasyncio.gatherfor structured concurrency and better error handling. - Never use
asyncio.sleep(0)as a yield point — if you need to yield control, you likely have a design issue.
- Use
pytestas the test framework. Nounittestsubclassing. - Name tests descriptively:
test_parse_response_raises_on_empty_body. - Use fixtures for setup/teardown. Prefer factory fixtures over complex fixture chains.
- Use parametrize for testing multiple inputs against the same logic.
- Test behavior, not implementation. Tests should survive refactoring.
- Aim for high coverage on domain logic, not on glue code.
- Use the
loggingmodule. Never useprint()for any form of output in production code. - Create a structured logger that is reusable across the application:
from pyagent import logging def get_logger(name: str) -> logging.Logger: logger = logging.getLogger(name) if not logger.handlers: handler = logging.StreamHandler() formatter = logging.Formatter( "%(asctime)s | %(name)s | %(levelname)s | %(message)s" ) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.DEBUG) return logger
- Use appropriate log levels:
DEBUGfor diagnostic detail,INFOfor operational events,WARNINGfor recoverable issues,ERRORfor failures,CRITICALfor fatal conditions.
A well-organized module follows this order:
- Module docstring
__all__(if applicable)- Imports (standard library → third-party → local)
- Constants
- Type aliases
- Exceptions
- Classes
- Functions
if __name__ == "__main__":block (scripts only)
- One concept per module. A module named
models.pyshould contain data models, not also utility functions and exception classes. - Use
__init__.pyto define the package's public API. Re-export only what external consumers need. - Keep
__init__.pythin. It should contain imports and__all__, not logic.
- Use
rufffor both formatting and linting. It replacesblack,isort,flake8, andpylintin a single tool. - Use
tyfor static type checking. - Line length: 88 characters (ruff default, matching black).
- Trailing commas on multi-line structures — they minimize diffs and prevent
syntax errors:
config = Config( host="localhost", port=8080, debug=True, )
- No commented-out code. Version control exists. Dead code is noise.