Sub-Skill: Python Best Practices
Purpose: Prevents the Python-specific mistakes LLMs make on autopilot — mutable defaults, bare excepts, missing guards, and subtle performance traps that pass review but break in production.
Rule classification
- MUST — load-bearing. Violating causes bugs, security issues, or invisible failures. Never break.
- SHOULD — default behavior. Deviation needs a documented reason in the code or PR.
- AVOID — usually wrong; documented exception inline where needed.
Where these rules don't strictly apply: test fixtures, generated code, throwaway scripts, REPL exploration, and tutorial snippets may legitimately differ. The rules below apply to production code paths and reusable libraries.
Rules
Type Hints
SHOULD: Annotate function signatures with types. def process(data) tells callers nothing. Use def process(data: list[str]) -> dict[str, int]:. Return type None should be explicit too. Exception: lambdas and short generator helpers in private scope.
SHOULD: Use built-in lowercase generics for Python 3.9+. list[str], dict[str, int], tuple[int, ...] rather than List, Dict, Tuple from typing.
MUST: Use Optional[X] or X | None for nullable parameters, never a bare default of None without a type annotation. def find(id: int) -> User | None: not def find(id):.
AVOID: Any as a shortcut. If the type is genuinely unknown, document why with a comment. Any silences the type checker and hides bugs. Exception: third-party libraries without stubs and explicit dynamic-data boundaries (e.g. JSON decode at the API edge).
Error Handling
MUST: Never use bare except:. It catches SystemExit, KeyboardInterrupt, and GeneratorExit. Always catch except Exception: at minimum, or a specific exception class.
# Wrong
try:
risky()
except:
pass
# Correct
try:
risky()
except ValueError as e:
logger.warning("Invalid value: %s", e)
MUST: Never silently swallow exceptions with pass. At minimum log the error. Silent failures produce ghost bugs that are impossible to trace.
SHOULD: Raise with context when re-raising. Use raise NewError("msg") from original_error to preserve the traceback chain, not raise NewError("msg") alone.
Common Pitfalls
MUST: Never use mutable default arguments. Python evaluates defaults once at function definition, not per call. The list or dict is shared across all calls.
# Wrong — items accumulates across calls
def append_item(val, items=[]):
items.append(val)
return items
# Correct
def append_item(val, items=None):
if items is None:
items = []
items.append(val)
return items
MUST: Guard script entry points with if __name__ == "__main__":. Without it, importing the module executes top-level code, breaking tests and imports.
MUST: Use with for file handles, sockets, and locks. Never open a file without a context manager. f = open(...) without with leaks handles on exceptions.
# Wrong
f = open("data.txt")
data = f.read()
f.close()
# Correct
with open("data.txt") as f:
data = f.read()
AVOID: String concatenation in loops. Each += on a string creates a new object. Collect into a list and call "".join(parts) at the end.
# Wrong — O(n^2) memory
result = ""
for word in words:
result += word + " "
# Correct
result = " ".join(words)
SHOULD: Use f-strings for string interpolation in Python 3.6+. Avoid % formatting or "Hello " + name. F-strings are faster, safer, and readable.
# Avoid
msg = "User %s has %d items" % (name, count)
# Prefer
msg = f"User {name} has {count} items"
AVOID: dict() constructor when a literal suffices. {} is faster and more idiomatic. dict(key=value) is only justified when keys are dynamic or come from variables.
Performance
SHOULD: Use list comprehensions or generator expressions instead of map/filter with lambda. Comprehensions are more readable and equally fast. Use generators when the full list is not needed at once.
# Avoid
result = list(map(lambda x: x * 2, items))
# Prefer
result = [x * 2 for x in items]
# For large data, use a generator
total = sum(x * 2 for x in items)
SHOULD: Use a set for membership lookups, not a list. x in list is O(n). x in set is O(1). Convert once, query many times.
# Wrong for repeated lookups
valid_ids = [1, 2, 3, ...]
if user_id in valid_ids: # O(n) every call
# Correct
valid_ids = {1, 2, 3, ...}
if user_id in valid_ids: # O(1)
Testing
SHOULD: Name test functions to describe the scenario, not just the function under test. test_process_returns_empty_dict_on_empty_input not test_process.
MUST: Never use assert statements in production code for validation. assert is stripped with python -O. Use explicit if checks with raise ValueError(...) for runtime validation.
MUST: Use pytest.raises as a context manager to assert exceptions, never wrap in try/except inside a test. A bare try/except can mask a missing exception.
# Wrong
def test_bad_input():
try:
process(None)
except ValueError:
pass # test passes even if no exception is raised
# Correct
def test_bad_input():
with pytest.raises(ValueError, match="input cannot be None"):
process(None)
SHOULD: Use @pytest.mark.parametrize to test the same logic with multiple inputs. Write one parameterized test instead of duplicating test functions for each case.
SHOULD: Place shared fixtures in conftest.py at the nearest common ancestor directory. Do not scatter fixtures across individual test files. Pytest auto-discovers conftest.py at every level.
MUST: Never return mutable objects directly from a pytest fixture. Each test must receive a fresh instance. Use factory fixtures that return a callable creating fresh objects. Reference: ERR-2026-014.
AVOID: Fixtures with side effects (network, DB) without explicit scope. Use scope="session" or scope="module" for expensive shared resources with proper teardown via yield.
SHOULD: Use the tmp_path fixture for filesystem tests instead of manual temporary directories. It auto-cleans and is process-safe.
Packaging
SHOULD: Include __init__.py in every package directory. Without it, Python 3 treats the directory as a namespace package, which breaks relative imports and tool discovery in many environments. Exception: explicit namespace packages (PEP 420) where the omission is documented intent.
AVOID: Imports from __init__.py inside the same package's submodules. This creates circular imports. Keep __init__.py as a re-export surface only, not a logic file.
SHOULD: Use pyproject.toml as the single source of project metadata. Avoid setup.py and setup.cfg for new projects. Modern tooling (pip, build, hatch, uv) all read pyproject.toml natively.
MUST: Pin exact versions in lock files but use ranges in pyproject.toml dependencies. Lock files (uv.lock, poetry.lock) ensure reproducibility; flexible ranges in project metadata allow resolver to find compatible versions.
SHOULD: Define CLI entry points in [project.scripts] instead of relying on python -m patterns. Entry points generate proper executables and integrate with system PATH.
Why This Sub-Skill Earns Stars
These rules target the exact failure modes that appear in LLM-generated Python:
- Mutable defaults and missing
__main__ guards are invisible bugs that only surface at runtime.
- Bare
except: and silent pass blocks make debugging take 10x longer.
- String concatenation in loops and list membership checks are performance traps that scale badly.
- Missing type annotations and
Any shortcuts defeat the entire value of static analysis.
- Skipping
with for file handles causes resource leaks under load.
None of these are caught by syntax checkers. All of them appear in production incidents. The MUST/SHOULD/AVOID classification means the security/correctness rules are strict and the stylistic rules respect context.
1---2name: python3description: Apply when writing Python code. Type hints, error handling, mutable defaults, async patterns, and packaging conventions.4license: MIT5---67# Sub-Skill: Python Best Practices8<!-- target: ~2200 tokens (real tiktoken count) | 20 rules with severity classification -->910**Purpose:** Prevents the Python-specific mistakes LLMs make on autopilot — mutable defaults, bare excepts, missing guards, and subtle performance traps that pass review but break in production.1112## Rule classification1314- **MUST** — load-bearing. Violating causes bugs, security issues, or invisible failures. Never break.15- **SHOULD** — default behavior. Deviation needs a documented reason in the code or PR.16- **AVOID** — usually wrong; documented exception inline where needed.1718**Where these rules don't strictly apply:** test fixtures, generated code, throwaway scripts, REPL exploration, and tutorial snippets may legitimately differ. The rules below apply to **production code paths and reusable libraries**.1920---2122## Rules2324### Type Hints25261. **SHOULD: Annotate function signatures with types.** `def process(data)` tells callers nothing. Use `def process(data: list[str]) -> dict[str, int]:`. Return type `None` should be explicit too. *Exception: lambdas and short generator helpers in private scope.*27282. **SHOULD: Use built-in lowercase generics for Python 3.9+.** `list[str]`, `dict[str, int]`, `tuple[int, ...]` rather than `List`, `Dict`, `Tuple` from `typing`.29303. **MUST: Use `Optional[X]` or `X | None` for nullable parameters, never a bare default of `None` without a type annotation.** `def find(id: int) -> User | None:` not `def find(id):`.31324. **AVOID: `Any` as a shortcut.** If the type is genuinely unknown, document why with a comment. `Any` silences the type checker and hides bugs. *Exception: third-party libraries without stubs and explicit dynamic-data boundaries (e.g. JSON decode at the API edge).*3334---3536### Error Handling37385. **MUST: Never use bare `except:`.** It catches `SystemExit`, `KeyboardInterrupt`, and `GeneratorExit`. Always catch `except Exception:` at minimum, or a specific exception class.3940 ```python41 # Wrong42 try:43 risky()44 except:45 pass4647 # Correct48 try:49 risky()50 except ValueError as e:51 logger.warning("Invalid value: %s", e)52 ```53546. **MUST: Never silently swallow exceptions with `pass`.** At minimum log the error. Silent failures produce ghost bugs that are impossible to trace.55567. **SHOULD: Raise with context when re-raising.** Use `raise NewError("msg") from original_error` to preserve the traceback chain, not `raise NewError("msg")` alone.5758---5960### Common Pitfalls61628. **MUST: Never use mutable default arguments.** Python evaluates defaults once at function definition, not per call. The list or dict is shared across all calls.6364 ```python65 # Wrong — items accumulates across calls66 def append_item(val, items=[]):67 items.append(val)68 return items6970 # Correct71 def append_item(val, items=None):72 if items is None:73 items = []74 items.append(val)75 return items76 ```77789. **MUST: Guard script entry points with `if __name__ == "__main__":`.** Without it, importing the module executes top-level code, breaking tests and imports.798010. **MUST: Use `with` for file handles, sockets, and locks.** Never open a file without a context manager. `f = open(...)` without `with` leaks handles on exceptions.8182 ```python83 # Wrong84 f = open("data.txt")85 data = f.read()86 f.close()8788 # Correct89 with open("data.txt") as f:90 data = f.read()91 ```929311. **AVOID: String concatenation in loops.** Each `+=` on a string creates a new object. Collect into a list and call `"".join(parts)` at the end.9495 ```python96 # Wrong — O(n^2) memory97 result = ""98 for word in words:99 result += word + " "100101 # Correct102 result = " ".join(words)103 ```10410512. **SHOULD: Use f-strings for string interpolation in Python 3.6+.** Avoid `%` formatting or `"Hello " + name`. F-strings are faster, safer, and readable.106107 ```python108 # Avoid109 msg = "User %s has %d items" % (name, count)110111 # Prefer112 msg = f"User {name} has {count} items"113 ```11411513. **AVOID: `dict()` constructor when a literal suffices.** `{}` is faster and more idiomatic. `dict(key=value)` is only justified when keys are dynamic or come from variables.116117---118119### Performance12012114. **SHOULD: Use list comprehensions or generator expressions instead of `map`/`filter` with `lambda`.** Comprehensions are more readable and equally fast. Use generators when the full list is not needed at once.122123 ```python124 # Avoid125 result = list(map(lambda x: x * 2, items))126127 # Prefer128 result = [x * 2 for x in items]129130 # For large data, use a generator131 total = sum(x * 2 for x in items)132 ```13313415. **SHOULD: Use a `set` for membership lookups, not a `list`.** `x in list` is O(n). `x in set` is O(1). Convert once, query many times.135136 ```python137 # Wrong for repeated lookups138 valid_ids = [1, 2, 3, ...]139 if user_id in valid_ids: # O(n) every call140141 # Correct142 valid_ids = {1, 2, 3, ...}143 if user_id in valid_ids: # O(1)144 ```145146---147148### Testing14915016. **SHOULD: Name test functions to describe the scenario, not just the function under test.** `test_process_returns_empty_dict_on_empty_input` not `test_process`.15115217. **MUST: Never use `assert` statements in production code for validation.** `assert` is stripped with `python -O`. Use explicit `if` checks with `raise ValueError(...)` for runtime validation.15315418. **MUST: Use `pytest.raises` as a context manager to assert exceptions, never wrap in try/except inside a test.** A bare try/except can mask a missing exception.155156 ```python157 # Wrong158 def test_bad_input():159 try:160 process(None)161 except ValueError:162 pass # test passes even if no exception is raised163164 # Correct165 def test_bad_input():166 with pytest.raises(ValueError, match="input cannot be None"):167 process(None)168 ```16917019. **SHOULD: Use `@pytest.mark.parametrize` to test the same logic with multiple inputs.** Write one parameterized test instead of duplicating test functions for each case.17117220. **SHOULD: Place shared fixtures in `conftest.py` at the nearest common ancestor directory.** Do not scatter fixtures across individual test files. Pytest auto-discovers `conftest.py` at every level.17317421. **MUST: Never return mutable objects directly from a pytest fixture.** Each test must receive a fresh instance. Use factory fixtures that return a callable creating fresh objects. Reference: ERR-2026-014.17517622. **AVOID: Fixtures with side effects (network, DB) without explicit scope.** Use `scope="session"` or `scope="module"` for expensive shared resources with proper teardown via `yield`.17717823. **SHOULD: Use the `tmp_path` fixture for filesystem tests instead of manual temporary directories.** It auto-cleans and is process-safe.179180---181182### Packaging18318424. **SHOULD: Include `__init__.py` in every package directory.** Without it, Python 3 treats the directory as a namespace package, which breaks relative imports and tool discovery in many environments. *Exception: explicit namespace packages (PEP 420) where the omission is documented intent.*18518625. **AVOID: Imports from `__init__.py` inside the same package's submodules.** This creates circular imports. Keep `__init__.py` as a re-export surface only, not a logic file.18718826. **SHOULD: Use `pyproject.toml` as the single source of project metadata.** Avoid `setup.py` and `setup.cfg` for new projects. Modern tooling (pip, build, hatch, uv) all read `pyproject.toml` natively.18919027. **MUST: Pin exact versions in lock files but use ranges in `pyproject.toml` dependencies.** Lock files (`uv.lock`, `poetry.lock`) ensure reproducibility; flexible ranges in project metadata allow resolver to find compatible versions.19119228. **SHOULD: Define CLI entry points in `[project.scripts]` instead of relying on `python -m` patterns.** Entry points generate proper executables and integrate with system PATH.193194---195196## Why This Sub-Skill Earns Stars197198These rules target the exact failure modes that appear in LLM-generated Python:199200- Mutable defaults and missing `__main__` guards are invisible bugs that only surface at runtime.201- Bare `except:` and silent `pass` blocks make debugging take 10x longer.202- String concatenation in loops and list membership checks are performance traps that scale badly.203- Missing type annotations and `Any` shortcuts defeat the entire value of static analysis.204- Skipping `with` for file handles causes resource leaks under load.205206None of these are caught by syntax checkers. All of them appear in production incidents. The MUST/SHOULD/AVOID classification means the security/correctness rules are strict and the stylistic rules respect context.