Skip to content

Latest commit

 

History

History
1135 lines (816 loc) · 31.9 KB

File metadata and controls

1135 lines (816 loc) · 31.9 KB

Python Development Patterns

Idiomatic Python patterns and best practices for building robust, efficient, and maintainable applications.

PEP 8 Style Guide · The Zen of Python · Python docs

Core Principles

Readability Counts. Python prioritizes readability. Code should be obvious and easy to understand.

# Good: Clear and readable
def get_active_users(users: list[User]) -> list[User]:
    """Return only active users from the provided list."""
    return [user for user in users if user.is_active]


# Bad: Clever but confusing
def get_active_users(u):
    return [x for x in u if x.a]

Type Hints

typing module · PEP 484 · see python-typing.md for a deep dive

Basic Type Annotations

from typing import Any

def process_user(
    user_id: str,
    data: dict[str, Any],
    active: bool = True
) -> User | None:
    """Process a user and return the updated User or None."""
    if not active:
        return None
    return User(user_id, data)

Modern Type Hints (Python 3.9+)

Use built-in types directly as generics, and | for unions — no import from typing needed. This is the default to reach for; only fall back to typing.List/Dict/Optional when the code must also run on Python 3.8 or earlier.

# Python 3.9+ — use built-in types
def process_items(items: list[str]) -> dict[str, int]:
    return {item: len(item) for item in items}

# Python 3.8 and earlier — import from typing
from typing import List, Dict

def process_items(items: List[str]) -> Dict[str, int]:
    return {item: len(item) for item in items}

Type Aliases and TypeVar

from typing import TypeVar, Union

# Type alias for complex types
JSON = Union[dict[str, Any], list[Any], str, int, float, bool, None]

def parse_json(data: str) -> JSON:
    return json.loads(data)

# Generic types
T = TypeVar('T')

def first(items: list[T]) -> T | None:
    """Return the first item or None if list is empty."""
    return items[0] if items else None

Protocol-Based Duck Typing

from typing import Protocol

class Renderable(Protocol):
    def render(self) -> str:
        """Render the object to a string."""

def render_all(items: list[Renderable]) -> str:
    """Render all items that implement the Renderable protocol."""
    return "\n".join(item.render() for item in items)

Error Handling Patterns

Exceptions tutorial · Built-in exceptions

Specific Exception Handling

# Good: Catch specific exceptions
def load_config(path: str) -> Config:
    try:
        with open(path) as f:
            return Config.from_json(f.read())
    except FileNotFoundError as e:
        raise ConfigError(f"Config file not found: {path}") from e
    except json.JSONDecodeError as e:
        raise ConfigError(f"Invalid JSON in config: {path}") from e

# Bad: Bare except
def load_config(path: str) -> Config:
    try:
        with open(path) as f:
            return Config.from_json(f.read())
    except:
        return None  # Silent failure!

Exception Chaining

def process_data(data: str) -> Result:
    try:
        parsed = json.loads(data)
    except json.JSONDecodeError as e:
        # Chain exceptions to preserve the traceback
        raise ValueError(f"Failed to parse data: {data}") from e

Custom Exception Hierarchy

class AppError(Exception):
    """Base exception for all application errors."""
    pass

class ValidationError(AppError):
    """Raised when input validation fails."""
    pass

class NotFoundError(AppError):
    """Raised when a requested resource is not found."""
    pass

# Usage
def get_user(user_id: str) -> User:
    user = db.find_user(user_id)
    if not user:
        raise NotFoundError(f"User not found: {user_id}")
    return user

Context Managers

contextlib · Context Manager Protocol

Resource Management

# Good: Using context managers
def process_file(path: str) -> str:
    with open(path, 'r') as f:
        return f.read()

# Bad: Manual resource management
def process_file(path: str) -> str:
    f = open(path, 'r')
    try:
        return f.read()
    finally:
        f.close()

Custom Context Managers

from contextlib import contextmanager

@contextmanager
def timer(name: str):
    """Context manager to time a block of code."""
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f"{name} took {elapsed:.4f} seconds")

# Usage
with timer("data processing"):
    process_large_dataset()

Context Manager Classes

class DatabaseTransaction:
    def __init__(self, connection):
        self.connection = connection

    def __enter__(self):
        self.connection.begin_transaction()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.connection.commit()
        else:
            self.connection.rollback()
        return False  # Don't suppress exceptions

# Usage
with DatabaseTransaction(conn):
    user = conn.create_user(user_data)
    conn.create_profile(user.id, profile_data)

Comprehensions and Generators

Generator expressions · List/set/dict displays

List Comprehensions

# Good: List comprehension for simple transformations
names = [user.name for user in users if user.is_active]

# Bad: Manual loop
names = []
for user in users:
    if user.is_active:
        names.append(user.name)

# Chained filters are just conditions to merge with `and` — still one readable comprehension
# Bad: unnecessary second `if` clause
result = [x * 2 for x in items if x > 0 if x % 2 == 0]

# Good: merge the conditions, no need to regress to a manual loop
result = [x * 2 for x in items if x > 0 and x % 2 == 0]

# Reach for a full function only once there's genuine multi-step logic,
# not just an extra filter:
def filter_and_transform(items: Iterable[int]) -> list[int]:
    result = []
    for x in items:
        if x > 0 and x % 2 == 0:
            transformed = expensive_multi_step_transform(x)
            if transformed is not None:
                result.append(transformed)
    return result

Generator Expressions

# Good: Generator for lazy evaluation
total = sum(x * x for x in range(1_000_000))

# Bad: Creates large intermediate list
total = sum([x * x for x in range(1_000_000)])

Generator Functions

def read_large_file(path: str) -> Iterator[str]:
    """Read a large file line by line."""
    with open(path) as f:
        for line in f:
            yield line.strip()

# Usage
for line in read_large_file("huge.txt"):
    process(line)

Data Classes and Named Tuples

dataclasses · collections.namedtuple

Data Classes

Dataclasses are a great alternative to dictionaries or regular classes for simple data containers. They automatically generate __init__, __repr__, and __eq__ methods, and are strict about their attributes (typing is easier than using TypedDict).

from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class User:
    """User entity with automatic __init__, __repr__, and __eq__."""
    id: str
    name: str
    email: str
    created_at: datetime = field(default_factory=datetime.now)
    is_active: bool = True

    def __post_init__(self):
        """Validate fields right after __init__ runs."""
        if "@" not in self.email:
            raise ValueError(f"Invalid email: {self.email}")

# Usage
user = User(id="123", name="Alice", email="alice@example.com")

ClassVar and InitVar

from dataclasses import dataclass, field, InitVar
from typing import ClassVar

@dataclass
class User:
    # ClassVar — class-level attribute, excluded from __init__ and __repr__
    registry: ClassVar[dict[str, "User"]] = {}

    id: str
    name: str
    tags: list[str] = field(default_factory=list)

    def __post_init__(self) -> None:
        User.registry[self.id] = self


# InitVar — passed to __post_init__ but NOT stored as a field on the instance
@dataclass
class Connection:
    host: str
    port: int
    password: InitVar[str]   # available in __post_init__, then discarded

    def __post_init__(self, password: str) -> None:
        self._client = connect(self.host, self.port, password)

Named Tuples

A NamedTuple is more memory-efficient than a dataclass because it stores data exactly like a tuple. It is immutable and hashable by default. Use a dataclass when you need mutability.

from typing import NamedTuple

class Point(NamedTuple):
    """Immutable 2D point."""
    x: float
    y: float

    def distance(self, other: 'Point') -> float:
        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5

p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance(p2))  # 5.0

Decorators

PEP 318 · functools.wraps · ParamSpec

Decorators modify the behavior of functions or classes without changing their source. Always use functools.wraps to preserve the original function's metadata (name, docstring, signature).

import functools
import time

def timer(func: Callable) -> Callable:
    """Decorator to time function execution."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

# slow_function() prints: slow_function took 1.0012s

Parametrized Decorators

When a decorator needs arguments, add one extra level of nesting: a factory that returns the actual decorator.

def retry(max_attempts: int = 3, exceptions: tuple = (Exception,)):
    """Decorator factory — called with arguments, returns the actual decorator."""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == max_attempts - 1:
                        raise
        return wrapper
    return decorator

@retry(max_attempts=5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str) -> str: ...

Typing Decorators with ParamSpec

Without ParamSpec, a decorator's wrapper has type (*args: Any, **kwargs: Any) -> R, losing all parameter names and types from the decorated function. ParamSpec preserves the full signature.

from typing import ParamSpec, TypeVar
from collections.abc import Callable

P = ParamSpec("P")
R = TypeVar("R")

def retry(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        for attempt in range(3):
            try:
                return func(*args, **kwargs)
            except Exception:
                if attempt == 2:
                    raise
        raise RuntimeError("unreachable")
    return wrapper

@retry
def fetch(url: str, timeout: int = 30) -> str: ...

fetch("https://example.com", timeout=10)   # type checker knows both args and return type
fetch(timeout=10)                          # type error — url is required

Concurrency Patterns

concurrent.futures · asyncio · threading

Threading for I/O-Bound Tasks

import concurrent.futures

def fetch_url(url: str) -> str:
    """Fetch a URL (I/O-bound operation)."""
    import urllib.request
    with urllib.request.urlopen(url) as response:
        return response.read().decode()

def fetch_all_urls(urls: list[str]) -> dict[str, str]:
    """Fetch multiple URLs concurrently using threads."""
    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
        future_to_url = {executor.submit(fetch_url, url): url for url in urls}
        results = {}
        for future in concurrent.futures.as_completed(future_to_url):
            url = future_to_url[future]
            try:
                results[url] = future.result()
            except Exception as e:
                results[url] = f"Error: {e}"
    return results

Multiprocessing for CPU-Bound Tasks

def process_data(data: list[int]) -> int:
    """CPU-intensive computation."""
    return sum(x ** 2 for x in data)

def process_all(datasets: list[list[int]]) -> list[int]:
    """Process multiple datasets using multiple processes."""
    with concurrent.futures.ProcessPoolExecutor() as executor:
        results = list(executor.map(process_data, datasets))
    return results

Async/Await for Concurrent I/O

import asyncio

async def fetch_async(url: str) -> str:
    """Fetch a URL asynchronously."""
    import aiohttp
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def fetch_all(urls: list[str]) -> dict[str, str]:
    """Fetch multiple URLs concurrently."""
    tasks = [fetch_async(url) for url in urls]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return dict(zip(urls, results))

Memory and Performance

__slots__ · Generator expressions

Using slots for Memory Efficiency

# Bad: Regular class uses __dict__ (more memory)
class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

# Good: __slots__ reduces memory usage
class Point:
    __slots__ = ['x', 'y']

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

Generator for Large Data

# Bad: Returns full list in memory
def read_lines(path: str) -> list[str]:
    with open(path) as f:
        return [line.strip() for line in f]

# Good: Yields lines one at a time
def read_lines(path: str) -> Iterator[str]:
    with open(path) as f:
        for line in f:
            yield line.strip()

Properties

property

Properties let you add validation or computation to attribute access without changing the public interface — the Pythonic alternative to Java-style getX()/setX() methods.

class Temperature:
    def __init__(self, celsius: float = 0.0):
        self._celsius = celsius  # _ prefix signals "private by convention"

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError(f"Temperature below absolute zero: {value}")
        self._celsius = value

    @property
    def fahrenheit(self) -> float:
        """Computed, read-only property — no setter defined."""
        return self._celsius * 9 / 5 + 32


t = Temperature(100)
print(t.fahrenheit)  # 212.0
t.celsius = -300     # raises ValueError

Class Methods and Static Methods

classmethod · staticmethod

class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

    @classmethod
    def from_dict(cls, data: dict) -> "User":
        """Alternative constructor — cls is the class itself, so subclasses
        that inherit this factory will instantiate themselves, not User."""
        return cls(name=data["name"], email=data["email"])

    @staticmethod
    def validate_email(email: str) -> bool:
        """Pure utility — no access to cls or self needed.
        Belongs logically to User but is completely stateless."""
        return "@" in email and "." in email.split("@")[-1]


user = User.from_dict({"name": "Alice", "email": "alice@example.com"})
User.validate_email("bad-email")  # False

Rule of thumb: use @classmethod when the method needs access to the class itself (factory patterns, class-level registries), use @staticmethod for stateless utilities that need neither self nor cls.

Dunder (Magic) Methods

Data Model reference

Dunder methods let your objects integrate seamlessly with Python's syntax and built-ins. The language calls them implicitly — you never call obj.__len__() directly, you call len(obj).

Representation

class Vector:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        """Unambiguous — shown in REPL, logs, and debuggers.
        Goal: eval(repr(obj)) == obj when practical."""
        return f"Vector({self.x!r}, {self.y!r})"

    def __str__(self) -> str:
        """Human-readable — shown by print() and str().
        Falls back to __repr__ when not defined."""
        return f"({self.x}, {self.y})"

Arithmetic and Comparison

    def __add__(self, other: "Vector") -> "Vector":
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar: float) -> "Vector":
        return Vector(self.x * scalar, self.y * scalar)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented  # Not False — lets Python try the reflected operation
        return self.x == other.x and self.y == other.y

    def __hash__(self) -> int:
        # Defining __eq__ without __hash__ makes the object unhashable (can't be in a set or dict key).
        # Rule: if two objects are equal, they must have the same hash.
        return hash((self.x, self.y))

    def __bool__(self) -> bool:
        """Truthiness — used by if obj:, while obj:, and bool(obj)."""
        return self.x != 0 or self.y != 0

Container Protocol

class Grid:
    def __init__(self, rows: int, cols: int):
        self._data = [[0] * cols for _ in range(rows)]

    def __len__(self) -> int:
        return len(self._data)

    def __getitem__(self, index: int) -> list[int]:
        return self._data[index]

    def __setitem__(self, index: int, value: list[int]) -> None:
        self._data[index] = value

    def __contains__(self, item: int) -> bool:
        return any(item in row for row in self._data)

Custom Iterator

class CountDown:
    def __init__(self, start: int):
        self.current = start

    def __iter__(self) -> "CountDown":
        return self  # The object is its own iterator

    def __next__(self) -> int:
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value


for n in CountDown(3):
    print(n)  # 3, 2, 1

Callable Objects

class Multiplier:
    def __init__(self, factor: float):
        self.factor = factor

    def __call__(self, value: float) -> float:
        return value * self.factor


double = Multiplier(2)
double(5)                      # 10 — called just like a function
list(map(double, [1, 2, 3]))   # [2, 4, 6]

Enums

enum

Enums replace magic strings and integers with named, type-safe constants. They prevent typos, enable exhaustive checking, and make intent explicit.

from enum import Enum, IntEnum, Flag, auto


class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3


# Enum members are singletons — compare with is or ==
Color.RED is Color.RED   # True
Color.RED == 1           # False — Enum members are not plain ints

# Access by value or name
Color(1)           # Color.RED
Color["RED"]       # Color.RED
Color.RED.name     # "RED"
Color.RED.value    # 1

# Iteration
for color in Color:
    print(color.name, color.value)


# IntEnum — when you need integer comparison (e.g. HTTP status codes, priority queues)
class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


Priority.HIGH > Priority.LOW   # True


# auto() — values assigned automatically
class Direction(Enum):
    NORTH = auto()
    SOUTH = auto()
    EAST = auto()
    WEST = auto()


# Flag — for bitwise combination of options
class Permission(Flag):
    READ = auto()
    WRITE = auto()
    EXECUTE = auto()
    ALL = READ | WRITE | EXECUTE


user_perms = Permission.READ | Permission.WRITE
Permission.READ in user_perms      # True
Permission.EXECUTE in user_perms   # False

Abstract Base Classes

abc · collections.abc

ABCs enforce that subclasses implement a specific interface. The contract is explicit at class definition time rather than silently failing at call time.

from abc import ABC, abstractmethod
import math


class Shape(ABC):
    """Cannot be instantiated — exists only to define the interface."""

    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

    def describe(self) -> str:
        """Concrete methods are fine — shared by all subclasses."""
        return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"


class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        return 2 * math.pi * self.radius


Shape()            # TypeError: Can't instantiate abstract class Shape
circle = Circle(5)
circle.describe()  # "Area: 78.54, Perimeter: 31.42"

ABC vs Protocol: ABC enforces inheritance — subclasses must explicitly inherit from the ABC. Protocol uses structural subtyping (duck typing) — any class with the right methods satisfies it regardless of inheritance. Use Protocol for external or third-party types you cannot control; use ABC for your own class hierarchy where you want an enforced contract.

Collections Module

collections

The collections module provides specialized container types that solve common patterns more cleanly — and often more efficiently — than plain dicts and lists.

from collections import defaultdict, Counter, deque, ChainMap


# defaultdict — initializes missing keys automatically, avoiding KeyError
word_groups: defaultdict[int, list[str]] = defaultdict(list)
for word in ["apple", "banana", "cat", "dog"]:
    word_groups[len(word)].append(word)
# {5: ['apple'], 6: ['banana'], 3: ['cat', 'dog']}

graph: defaultdict[str, list[str]] = defaultdict(list)
graph["a"].append("b")  # No KeyError even for unseen keys


# Counter — count occurrences and tally
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
counts.most_common(2)   # [('apple', 3), ('banana', 2)]

# Counter supports arithmetic
a = Counter(["x", "y", "x"])
b = Counter(["x", "z"])
a + b   # Counter({'x': 3, 'y': 1, 'z': 1})
a - b   # Counter({'y': 1, 'x': 1})


# deque — O(1) appends and pops on both ends
# list.insert(0, ...) and list.pop(0) are O(n) — deque fixes that
buffer: deque[int] = deque(maxlen=3)  # fixed-size sliding window
for i in range(5):
    buffer.append(i)
# deque([2, 3, 4], maxlen=3) — oldest element auto-evicted

buffer.appendleft(0)   # O(1)
buffer.popleft()       # O(1)


# ChainMap — overlay multiple dicts, searched left-to-right
defaults = {"color": "red", "timeout": 30}
overrides = {"color": "blue"}
config = ChainMap(overrides, defaults)
config["color"]     # "blue" — from overrides
config["timeout"]   # 30 — fallback to defaults

Functools

functools

Caching

Cache the return value of a function keyed on its arguments. Subsequent calls with the same arguments return the cached result without re-executing the function.

import functools


# @cache — unbounded cache (Python 3.9+), simpler than lru_cache
@functools.cache
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


# @lru_cache — bounded with LRU eviction (use when memory is a concern)
@functools.lru_cache(maxsize=128)
def expensive_query(user_id: str) -> dict:
    return db.fetch_user(user_id)  # Only hits the database on cache miss


expensive_query.cache_info()    # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)
expensive_query.cache_clear()   # Invalidate the entire cache


# Cached functions require hashable arguments — lists are not hashable
# Bad
@functools.cache
def process(items: list[int]) -> int: ...

# Good — use tuples
@functools.cache
def process(items: tuple[int, ...]) -> int: ...

Partial Application

from functools import partial


def power(base: int, exponent: int) -> int:
    return base ** exponent


square = partial(power, exponent=2)
cube = partial(power, exponent=3)
square(5)   # 25
cube(3)     # 27

Itertools

itertools · Itertools Recipes

itertools provides building blocks for lazy, memory-efficient iteration. Compose these instead of writing manual loops over intermediate lists.

import itertools


# chain — flatten multiple iterables into one stream
list(itertools.chain([1, 2], [3, 4], [5]))               # [1, 2, 3, 4, 5]
list(itertools.chain.from_iterable([[1, 2], [3, 4]]))    # same, for a list-of-lists


# cycle — infinite repetition
spinner = itertools.cycle(["|", "/", "-", "\\"])
next(spinner)   # "|", then "/", then "-", then "\\" ...


# batched — chunk into fixed-size groups (Python 3.12+)
list(itertools.batched(range(10), 3))   # [(0,1,2), (3,4,5), (6,7,8), (9,)]

The module also provides: islice (lazy slice), groupby (group consecutive elements — sort first), product (Cartesian product), combinations / permutations, takewhile / dropwhile, accumulate, and more. See the official docs and the recipes section for the full reference.

Pathlib

pathlib

pathlib.Path replaces os.path with an object-oriented, readable API. Prefer it for all filesystem operations.

from pathlib import Path


# Construction — / operator joins path segments (cross-platform)
config = Path.home() / ".config" / "app" / "settings.json"
data_dir = Path("/var/data")


# Reading and writing
text = config.read_text(encoding="utf-8")
config.write_text('{"debug": false}', encoding="utf-8")
raw = config.read_bytes()
config.write_bytes(b"\x00\x01")


# Inspection
config.exists()     # True / False
config.is_file()    # True
config.is_dir()     # False


# Navigation
config.parent       # Path('/home/user/.config/app')
config.name         # 'settings.json'
config.stem         # 'settings'
config.suffix       # '.json'
config.suffixes     # ['.tar', '.gz'] for 'archive.tar.gz'


# Listing and globbing
for child in data_dir.iterdir():
    print(child)

for py_file in Path("src").rglob("*.py"):   # recursive
    process(py_file)


# Creating directories
Path("output/cache").mkdir(parents=True, exist_ok=True)


# Renaming and deleting
config.rename(config.with_suffix(".bak"))
config.unlink(missing_ok=True)   # delete; no error if already missing

Always prefer pathlib.Path over os.path — it composes cleanly, works cross-platform, and eliminates string manipulation for path operations.

Function Signatures: Keyword-Only and Positional-Only Parameters

PEP 3102 (keyword-only) · PEP 570 (positional-only)

Python lets you be explicit about how arguments must be passed at the call site.

# Keyword-only — parameters after * must be named by the caller
def create_user(name: str, *, email: str, active: bool = True) -> User:
    ...

create_user("Alice", email="alice@example.com")    # OK
create_user("Alice", "alice@example.com")           # TypeError


# Positional-only — parameters before / cannot be named by the caller
def distance(x: float, y: float, /) -> float:
    return (x ** 2 + y ** 2) ** 0.5

distance(3, 4)       # OK
distance(x=3, y=4)   # TypeError


# Combined: positional-only / normal * keyword-only
def move(x: float, y: float, /, speed: float = 1.0, *, animate: bool = False) -> None:
    """x, y: positional only. speed: either. animate: keyword only."""
    ...

When to use each:

  • Positional-only (/): parameter names are implementation details that you may rename without breaking callers. Common in math and C-extension functions where x, y, n are uninformative names.
  • Keyword-only (*): prevents subtle argument-order bugs, especially with booleans and flags. create_file("path", True, False) is ambiguous; create_file("path", overwrite=True, binary=False) is not.

Module API with __all__

The import system

__all__ is a list of names that defines the public interface of a module.

# mymodule.py
__all__ = ["PublicClass", "public_function"]


class PublicClass:
    """Part of the public API."""
    ...


class _PrivateHelper:
    """Internal — excluded from __all__, not exported."""
    ...


def public_function() -> None:
    ...


def _internal() -> None:
    ...

Two effects:

  1. from mymodule import * imports only names in __all__
  2. Documents intent — IDEs, help(), and documentation generators surface __all__ as the public API

Even when you never use import *, defining __all__ is good practice as a clear declaration of intent about what is stable and what is internal.

Anti-Patterns to Avoid

# Bad: Mutable default arguments
def append_to(item, items=[]):
    items.append(item)
    return items

# Good: Use None and create new list
def append_to(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

#=========
# Bad: Checking type with type()
if type(obj) == list:
    process(obj)

# Good: Use isinstance
if isinstance(obj, list):
    process(obj)

#==========
# Bad: Comparing to None with ==
if value == None:
    process()

# Good: Use is
if value is None:
    process()

#==========
# Bad: from module import *
from os.path import *

# Good: Explicit imports
from os.path import join, exists

#==========
# Bad: Bare except
try:
    risky_operation()
except:
    pass

# Good: Specific exception
try:
    risky_operation()
except SpecificError as e:
    logger.error(f"Operation failed: {e}")

#==========
# Avoid String Concatenation in Loops
# Bad: O(n²) due to string immutability
result = ""
for item in items:
    result += str(item)

# Good: O(n) using join
result = "".join(str(item) for item in items)

# Good: Using StringIO for building
from io import StringIO

buffer = StringIO()
for item in items:
    buffer.write(str(item))
result = buffer.getvalue()

Remember: Python code should be readable, explicit, and follow the principle of least surprise. When in doubt, prioritize clarity over cleverness.