About python
python is a set of coding standards for modern Python 3.12+ projects. It is intended for developers seeking consistent typing, module organization, and layered application design.
Key features
- 01Domain-oriented module structure
- 02Layered dependencies and thin entry points
- 03Type annotations and modern syntax
- 04Naming and import conventions
- 05Exception and logging guidance
Use cases
- 01Organizing a new Python application
- 02Defining reusable business logic services
- 03Reviewing function signatures and type hints
- 04Reducing circular imports and module coupling
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ imankulov/ skills --skill pythonAfter installing, type/pythonin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/data-structures.mddocstrings.mdlogical-grouping.mdmodule-organization.mdparallel-symmetry.mdurl-parsing.mdDetails
From the project READMEPython Coding Standards
Opinionated conventions for modern Python (3.12+) emphasizing type safety, clear function signatures, consistent module organization, and a layered architecture.
App Architecture
Screaming Architecture
Name modules after the domain — billing/, authentication/, annual_reports/ — so the
directory listing says what the app does rather than which framework it uses. Directories carry
the domain; the layers below live in files inside them (billing/services.py, billing/api.py).
A top-level models/ or views/ directory inverts that and scatters one domain across the tree.
Keep the modules flat — auth/, users/, and billing/ as siblings, not users/auth/ and
users/billing/. Nesting buries domains a level down, so the top-level listing stops telling
you at a glance what the app is about.
Layers
Dependencies flow downward. Each layer imports from the one below it, never the reverse.
+-------------------------------------------------------+
| api.py, tasks.py, admin.py, ... (entry points) |
+-------------------------------------------------------+
| imports
v
+-------------------------------------------------------+
| services.py (business logic) |
+-------------------------------------------------------+
| imports
v
+-------------------------------------------------------+
| models.py, types.py, const.py (data layer) |
+-------------------------------------------------------+
Entry points (api.py, tasks.py, admin.py, cli.py, etc.) are thin wrappers that call service functions. They handle request parsing, scheduling, and output formatting but contain no business logic. Entry points never import each other — they are peers that all depend on services; cross-imports create circular dependencies and tight coupling.
services.py holds all business logic. Service functions are reusable across any entry point — an API endpoint, a background task, a CLI command, and an admin action can all call the same service function.
A service takes structured input and returns structured output. It may do whatever I/O the work requires — read a file it was given a path to, query the database, call a third-party API — but it never touches the user-facing edge: no prompting or argument parsing, no coercing raw strings into typed values, no reaching into an HTTP request or another framework object, no formatting results for display. All of that belongs in the wrapper.
Data layer (models.py, types.py, const.py) defines data structures with zero
business logic. const.py must have no internal imports — it's safe to import from
anywhere.
Put all imports at the top of the module. Defer an import inside a function only to break a circular dependency; needing this often means the module structure needs refactoring.
Entry Point Naming Convention
Service functions own the base name. Entry points that wrap them add a suffix for their
layer, so from myapp.services import create_user works everywhere without aliasing.
| Layer | Suffix | Example |
|---|---|---|
| Service (business logic) | (none) | create_user() |
| API endpoint | _api | create_user_api() |
| CLI command | _cli | create_user_cli() |
| Background task | _task | create_user_task() |
Functions
- Type-annotate parameters and return values when the type carries information. If the only honest type is
Any(a wrapper around an optional/dynamically-imported library, a generic decorator, a**kwargspassthrough), leave it unannotated —Anyadds noise without helping the reader, the type checker, or the IDE. - Docstrings carry no type information — types live in the signature. See Docstrings and comments for how much to write.
- For functions with more than 3 parameters, use keyword-only arguments (
*). - Prefix internal helper functions with underscore and keep their docstrings to one line.
- Inline a helper that has one caller, unless it holds logic worth testing on its own — a predicate, a mapper, a conversion. Naming those lets a test drive them with hand-built inputs instead of setting up everything the caller needs; otherwise a second name for one function's body buys nothing.
- Order a file top-down like a newspaper (Robert C. Martin's Clean Code): public functions first, helpers below, a single-caller
_helperdirectly under its caller. Group Pydantic models near the top so they're defined before use.
The extended docstring form below is what a function looks like once it has earned all three sections; most functions stop at the one-line summary.
def process_user_data(*, user_id: int, name: str, email: str, age: int) -> UserData:
"""Process and validate user data.
Args:
user_id: Unique identifier for the user
name: User's full name
email: User's email address
age: User's age in years
Returns:
Processed user data with validation status
Raises:
ValueError: If the email is malformed or the age is negative
"""
...
def _format_name(first: str, last: str) -> str:
"""Combine first and last name into full name."""
return f"{first} {last}".strip()
Exceptions
Service functions signal failure by raising. List every exception a function raises on purpose
in the docstring's Raises: section — that contract is what entry points read to decide what
to catch and how to translate it.
Reach for the built-ins first: ValueError for bad input, RuntimeError for a broken
invariant. When the app needs its own, define one hierarchy in myapp/exceptions.py — beside
the app's __init__.py, a level above the modules that raise them — with MyAppError as the
base and MyAppAuthenticationError and siblings under it. Modules don't get their own
exceptions.py, and a failure mode rarely earns its own class; callers catch the base far
more often than the leaf.
Leading Underscores
A single leading underscore says "internal — don't call or import this from outside this module." Reserve it for two cases where outside use would cause a real problem:
- Internal functions and methods. A
_helpermay skip validation the public API enforces, hold an invariant of the surrounding function, or change shape at any time. - Mutable singleton state used for lazy initialization (
_queue,_started,_patched,_server_url). Outside code mutating it would break the module's invariants.
If another module needs to import it, it's not internal — drop the underscore. from foo import _bar is a lie: either keep the function truly internal, or move it to a shared module and remove the prefix. When extracting a helper into a shared utils.py, drop the underscore — it was internal to the old module, not the new one.
Do not prefix module-level constants or classes. UPPER_CASE already conveys "constant"; class privacy comes from where the class lives (don't re-export it, keep it next to its caller). Use MAX_RETRIES and RequestTracer, not _MAX_RETRIES or _RequestTracer.
Dunders (__init__, __enter__, …) are Python's protocol, not this convention.
Import Hygiene
Import your own modules under their real name — aliasing internal code hides where a symbol
came from. When a service function and the entry point wrapping it would collide, apply the
layer suffix rather than import ... as. Reserve aliases for a genuine collision between two
third-party imports and for their conventional shorthands (import pandas as pd).
Shared utilities belong in a utility module. When a second module needs a helper first
written inside another module, move it to a shared location (utils.py, helpers.py).
Don't let module_b depend on module_a just because module_a defined a generic
helper first.
Type Hints (PEP 604 and 695)
Use modern union syntax (list[str], dict[str, int] | None). Never import List,
Dict, Optional, or Union from typing for basic types.
Declare generics with PEP 695 type parameters — class Codec[T]:,
def first[T](items: list[T]) -> T: — not TypeVar plus Generic.
Naming
The same rules cover variables, functions, classes, packages, and module filenames.
Write words out. Prefer document to doc, request_id to req_id, configuration
to cfg. Dropped vowels and truncations save a few keystrokes and cost every later reader a
guess. The exceptions are the short forms that are more familiar than what they stand for —
id, url, json, csv, http — and even those go unabbreviated when there's any doubt.
Always separate words. snake_case for variables, functions, and module filenames;
CamelCase for classes. Never run two words together without a boundary: parse_payload,
not parsepayload; RetryPolicy, not Retrypolicy; sync_state.py, not syncstate.py.
Treat abbreviations as single words in CamelCase, capitalizing only the first letter:
JsonParser, ApiClient, DbConnection, HttpResponse, SqlQuery — not JSONParser.
Logging
Follow the project's logging conventions when its instructions define one. Otherwise use the
stdlib logging module and pass values as lazy %s arguments rather than interpolating them:
logger.warning("Vector search failed: %s", exc), not an f-string.
References
For detailed patterns on specific topics, load these as needed:
- Data structures — Pydantic vs dicts, enums, interfaces, TypedDict
- Docstrings and comments — how much to write, when sections earn their place
- Module organization — types.py, const.py placement rules
- Logical grouping — keeping fields, constants, and steps grouped by concern
- Parallel symmetry — naming and shape when two systems process the same input
- URL parsing — furl for all URL manipulation
Verification
After writing Python code, verify the two rules that live in references and are easiest to miss:
- Fields, constants, and processing steps grouped by concern, in the same order everywhere
- Systems processing the same input in parallel share prefixes, guards, and data shapes