Python Development Best Practices

SkillDocs & knowledge

Python best practices for code style, type hints, error handling, naming, documentation, and modern patterns based on PEP 8, PEP 257, PEP 20, and Google Python Style Guide

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Python Development Best Practices skill

What this skill tells your AI

The instructions your AI receives, as published by taurgis/has-marstek-local-api in .agents/skills/python-development/SKILL.md and read by ahel’s review.

Expert-level Python development guidance based on official sources: PEP 8, PEP 20 (Zen of Python), PEP 257, and Google Python Style Guide.

The Zen of Python (PEP 20)

Core principles that guide all Python decisions:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Flat is better than nested.
Readability counts.
Errors should never pass silently (unless explicitly silenced).
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
If the implementation is hard to explain, it's a bad idea.

Code Style (PEP 8)

Indentation & Line Length

RuleStandard
Indentation4 spaces (never tabs)
Line length79 characters (72 for docstrings/comments)
Blank lines2 before top-level definitions, 1 between methods

Imports

# Correct order: stdlib, third-party, local
import os
import sys

import requests

from myproject import utils

Rules:

  • One import per line (except from X import a, b, c)
  • Absolute imports preferred over relative
  • Never use from module import *
  • Group imports with blank lines between groups

Whitespace

# Correct
spam(ham[1], {eggs: 2})
x = 1
y = 2
if x == 4:
    print(x, y)

# Wrong
spam( ham[ 1 ], { eggs: 2 } )
x             = 1
y             = 2

Avoid extraneous whitespace:

  • Inside brackets: spam(ham[1]) not spam( ham[ 1 ] )
  • Before commas/colons: if x == 4: not if x == 4 :
  • Around = in keyword arguments: func(arg=value) not func(arg = value)

Binary Operators

Break before binary operators for readability (Knuth style):

# Correct
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction)

Naming Conventions

TypeConventionExample
Packages/Moduleslower_with_undermy_module.py
ClassesCapWordsMyClass
ExceptionsCapWords + Error suffixValidationError
Functions/Methodslower_with_undercalculate_total()
ConstantsCAPS_WITH_UNDERMAX_VALUE
Instance variableslower_with_underself.user_name
Protected_single_leading_underscore_internal_method()
Private (name mangling)__double_leading__private_attr

Critical rules:

  • Never use l, O, or I as single-character names
  • Avoid abbreviations unless universally understood
  • Names should reflect usage, not implementation

Type Annotations

Always Annotate

from __future__ import annotations
from typing import Any
from collections.abc import Sequence, Mapping

def process_items(
    items: Sequence[str],
    config: Mapping[str, Any] | None = None,
) -> list[str]:
    """Process items with optional config."""
    ...

Key Rules

PatternCorrectWrong
Optional`strNone`
Union`intstr`
Collectionslist[int]List[int] (old style)
Abstract typesSequence[T], Mapping[K, V]list, dict in signatures
Forward referenceUse from __future__ import annotationsString quotes

Spacing

# Correct
def func(a: int = 0) -> int:  # Space around = with annotation
    code: int  # Space after colon

# Wrong
def func(a:int=0) -> int:  # Missing spaces

Docstrings (PEP 257)

One-liners

def calculate_area(radius: float) -> float:
    """Return the area of a circle with given radius."""
    return 3.14159 * radius ** 2

Multi-line (Google Style)

def fetch_data(
    url: str,
    timeout: int = 30,
    retries: int = 3,
) -> dict[str, Any]:
    """Fetch data from a remote URL.

    Retrieves JSON data from the specified URL with automatic
    retry logic on transient failures.

    Args:
        url: The URL to fetch data from.
        timeout: Request timeout in seconds.
        retries: Maximum number of retry attempts.

    Returns:
        Parsed JSON response as a dictionary.

    Raises:
        ConnectionError: If unable to connect after all retries.
        ValueError: If response is not valid JSON.
    """

Class Docstrings

class DataProcessor:
    """Process and transform data records.

    Handles validation, transformation, and serialization of
    incoming data records.

    Attributes:
        batch_size: Number of records to process per batch.
        strict_mode: Whether to fail on first error.
    """

    def __init__(self, batch_size: int = 100) -> None:
        """Initialize the processor.

        Args:
            batch_size: Records per batch (default: 100).
        """

Exception Handling

Use Specific Exceptions

# Correct: Specific exception with context
def connect(port: int) -> Connection:
    if port < 1024:
        raise ValueError(f"Port must be >= 1024, got {port}")
    try:
        return _establish_connection(port)
    except OSError as err:
        raise ConnectionError(f"Failed to connect on port {port}") from err

# Wrong: Bare except, generic exception
try:
    do_something()
except:  # Never do this
    pass

Exception Rules

DoDon't
Catch specific exceptionsUse bare except:
Use raise X from Y for chainingLose original traceback
Add Error suffix to exception classesName exceptions without Error
Limit try blocks to minimal codeWrap too much code in try
Use finally for cleanupForget to close resources

Don't Use Assert for Validation

# Wrong: assert can be disabled with -O
def process(value: int) -> int:
    assert value >= 0, "Value must be non-negative"  # Don't do this
    return value * 2

# Correct: Use explicit validation
def process(value: int) -> int:
    if value < 0:
        raise ValueError(f"Value must be non-negative, got {value}")
    return value * 2

Comprehensions & Generators

Keep Comprehensions Simple

# Correct: Clear and readable
result = [x**2 for x in range(10) if x % 2 == 0]

# Wrong: Too complex - use a loop instead
result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]

Prefer Generators for Large Data

# Memory efficient
def process_large_file(path: str):
    with open(path) as f:
        yield from (line.strip() for line in f if line.strip())

Resource Management

Always Use Context Managers

# Correct
with open("file.txt") as f:
    content = f.read()

# Correct: Multiple resources
with (
    open("input.txt") as infile,
    open("output.txt", "w") as outfile,
):
    outfile.write(infile.read())

Mutable Default Arguments

Never use mutable defaults:

# Wrong: Mutable default shared across calls
def append_to(element, to=[]):  # Bug!
    to.append(element)
    return to

# Correct: Use None sentinel
def append_to(element, to: list | None = None) -> list:
    if to is None:
        to = []
    to.append(element)
    return to

Modern Python Features

Use f-strings (Not % or .format)

name = "World"
# Correct
message = f"Hello, {name}!"

# Prefer f-string debugging
value = 42
print(f"{value=}")  # Prints: value=42

Logging: Use %-formatting

import logging

logger = logging.getLogger(__name__)

# Correct: Lazy evaluation, structured logging
logger.info("Processing %d items for user %s", count, user_id)

# Wrong: f-string evaluated even if level disabled
logger.info(f"Processing {count} items for user {user_id}")

Use Walrus Operator When It Improves Readability

# Good use case: Avoid repeated computation
if (n := len(data)) > 10:
    print(f"Processing {n} items")

Code Smells to Avoid

SmellWhy It's BadFix
from module import *Pollutes namespaceImport explicitly
Nested functions > 2 deepHard to followExtract to methods
Methods > 50 linesToo complexSplit into smaller methods
except Exception:Catches too muchCatch specific exceptions
Global mutable stateSide effects, testing issuesPass as arguments
Magic numbersUnclear intentUse named constants
Boolean parametersUnclear at call siteUse enums or keyword args

Testing Conventions

import pytest

class TestCalculator:
    """Tests for the Calculator class."""

    def test_add_positive_numbers(self) -> None:
        """Addition of positive numbers returns correct sum."""
        assert Calculator().add(2, 3) == 5

    def test_divide_by_zero_raises(self) -> None:
        """Division by zero raises ZeroDivisionError."""
        with pytest.raises(ZeroDivisionError):
            Calculator().divide(1, 0)

Ruff Rules to Enable

Recommended rule selection for pyproject.toml:

[tool.ruff.lint]
select = [
    "E", "F", "W",      # pycodestyle, Pyflakes
    "I",                # isort
    "UP",               # pyupgrade
    "B",                # flake8-bugbear
    "C4",               # flake8-comprehensions
    "SIM",              # flake8-simplify
    "PT",               # flake8-pytest-style
    "RUF",              # Ruff-specific
]

Quick Reference Checklist

Before committing Python code:

  • All functions have type annotations
  • Public APIs have docstrings with Args/Returns/Raises
  • No bare except: clauses
  • No mutable default arguments
  • Imports are organized (stdlib, third-party, local)
  • No magic numbers (use constants)
  • Context managers for resources
  • Specific exception types with helpful messages
  • Line length ≤ 79 characters
  • Passes ruff check and mypy --strict

Signals

GitHub stars
35
Forks
5
Last commit
Sep 2026
Advanced
Item type
skill
Key
python-development-taurgis
Source
github.com/taurgis/has-marstek-local-api