Python Code Style
This skill provides comprehensive Python coding standards that must be followed when writing any Python code. The standards emphasize clean code through well-named functions, early failure principles, type safety with pydantic models, and structured logging.
After writing code, you MUST note the code rules that you have applied to the code, i.e. 1.1: No code comments.
When to Use This Skill
Apply these coding standards whenever:
- •Writing new Python code
- •Editing existing Python files
- •Reviewing Python code
- •Creating Python code snippets or examples
Rules
1. Apply Minimum Possible Change
- •When writing code, if you see existing code that violates this coding guideline, do not change it unless explicitly told to do so. This is to avoid changing too many things at once.
2. Code Comments
- •Do not write code comments. Separate code into clear logical blocks, and if we need to name such a block, put it in a well named function
- •Do not write docstrings
- •Keep in code comments that are in a plan that are indicating where new code should go and are not meant to persist in the final source code.
3. Functions
- •Put private functions at the bottom of the file
- •Avoid nesting functions within other functions
- •Default functions to private. Make them public only when used by another module.
- •Prefix private functions with an _ as per python convention
- •Nested functions should not have the _ prefix
4. Error handling
You must follow this diligently when writing all code:
- •The most important philosophy to follow is: fail as early as possible in code execution
- •Let the stack traces do the talking. Avoid wrapping exceptions
- •Never implement default handling unless explicitly instructed (i.e. using dict.get() unless it's completely core to the algorithm. We typically want to know if there's no data present).
5. Code Organisation
- •Place all source files in the
src/dir - •Always use relative imports
6. Naming
- •Prefer functional programming naming conventions names like:
name_frequency()vsget_name_frequency(). - •Use CamelCase for acronyms in class names, treating them as words:
DataForSeoTaskClientvsDataForSEOTaskClient
7. Type Hints
- •
Always add typehints to function signatures
- •
Omit None from function return types by default, unless it's not clear otherwise.
- •
Use built-in type hints (e.g.,
list,dict,tuple) instead of importing from typing module - •
Prefer
list[str]overList[str]from typing - •
Prefer
dict[str, int]overDict[str, int]from typing - •
Never use invalid types in function signatures for None:
- •Bad:
value: int = None - •Good:
value: Optional[int] = None
- •Bad:
- •
Prefer returning a pydantic type instead of a confusing tuple:
- •Bad:
def fn() -> tuple[list[int], list[int], list[int]] - •Good:
def fn(): FnResult, wherepyclass FnResult: user_ids: list[int] result_ids: list[int] other_ids: list[int]
- •Bad:
- •
Do not type with just
dict. Use a pydantic type with clear field names.- •Bad:
def fn(record: dict) -> dict - •Good:
def fn(record: Record): FnResult
- •Bad:
- •
Prefer pydantic BaseModels to python @dataclasses for new code
8. Return Types
- •Use pydantic BaseModel over complex dictionaries for return types containing more than one field
- •Use pydantic BaseModel types for type safety, better IDE support, and clearer interfaces
- •Example:
# Good: type-safe and clear
class SearchResult(BaseModel):
query: str
position: Optional[int]
total_results: int
def search(query: str) -> SearchResult:
return SearchResult(query=query, position=5, total_results=100)
# Bad: complex dicts are error-prone
def search(query: str) -> dict:
return {"query": query, "position": 5, "total_results": 100}
# Bad: tuples are confusing when passed around
def search(query: str) -> tuple[str, int, int]:
return query, 5, 100
9. Logging
- •All code should include logging
- •It should verify that code is working as expected
- •It should be placed so that, when issues arise, we can quickly pinpoint the problem, but not so many that we cannot read the code.
- •Use %s with arguments, vs f strings when logging, i.e.
log.info("Upload File: status=started id=%s, name=%s", id name) - •Use the format:
Upload File: status=started id=1, name=test.csv Upload File: status=done id=1, name=test.csv Upload File: status=error id=1, name=test.csv, reason=timed_out, message=timed out after 5000ms
2.10. Pydantic
- •
ALWAYS use TypeAdapter when converting
list[dict]tolist[Model]Whenever you have a list of raw data (dicts from API responses, cache entries, database rows, etc.) and need to convert to a list of pydantic models, use TypeAdapter to deserialize in one operation:
pyfrom pydantic import TypeAdapter # Good: TypeAdapter deserializes all items in one call accounts = TypeAdapter(list[GoogleAdsAccount]).validate_python(accounts_raw) # Bad: List comprehension with model_validate accounts = [GoogleAdsAccount.model_validate(_) for _ in accounts_raw] # Very Bad: Manual loop accounts = [] for item in accounts_raw: accounts.append(GoogleAdsAccount.model_validate(item))Key points:
- •Use
.validate_python()method (NOT.model_validate()) - •Applies to: API responses, cache entries, DB query results, file parsing
- •More performant and cleaner than loops or list comprehensions
- •Use