This guide reflects ArjanCodes' current Python style principles and modern best practices as of 2024.
Arjan often argues that Python developers over-engineer with classes when simple functions would suffice.
- Prefer Functions Over Classes: If a class only has an
__init__and one method, it should probably just be a function. - Composition over Inheritance: Instead of deep inheritance trees, use composition. Pass objects into other objects to build complex behavior.
- Single Responsibility Principle (SRP): A function or class should do exactly one thing. If you find yourself using the word "and" to describe what a function does, it’s too big.
Modern Python Project Layout:
project/
├── src/
│ └── package_name/
│ ├── __init__.py
│ └── modules...
├── tests/
├── docs/
├── pyproject.toml
└── README.md
Key Configuration:
- Use
pyproject.tomlfor modern Python project configuration - Isolate source code in
src/directory - Separate tests, documentation, and configuration
- Use proper package structure with
__init__.pyfiles
- Prefer Functions Over Classes: If a class only has an
__init__and one method, it should probably just be a function - Composition over Inheritance: Use composition instead of deep inheritance trees
- Single Responsibility Principle (SRP): A function or class should do exactly one thing
- Avoid "God Objects": Break down large classes into focused, single-responsibility components
Guard Clauses: Check for error/exit conditions first and return early
def process(data):
if data is None or "value" not in data:
return
# logic here...Registry Pattern: Replace long if-elif chains with dictionaries
operations = {
"add": add_function,
"multiply": multiply_function,
}
result = operations.get(operation, default_function)()Python 3.12+ Generics:
# Modern syntax — replaces old TypeVar approach
def func[T]() -> T:
return T()
# Bounded types
class Container[T: Mapping]:
pass
# Constrained types
class Calculator[T: (int, float)]:
passDictionary Operators:
# Python 3.9+ merge and update
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
merged = dict1 | dict2 # {'a': 1, 'b': 3, 'c': 4}Pattern Matching:
# Python 3.10+ match statement
match command:
case {"type": "move", "x": x, "y": y}:
move(x, y)
case {"type": "quit"}:
quit()Assignment Expressions:
# Walrus operator for cleaner code
if (n := len(items)) > 10:
print(f"List is too long ({n} elements)")Dataclasses:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Automatically generates __init__, __repr__, __eq__Type Annotations (modern style):
# Use built-in generics — NOT typing.Dict, typing.List, typing.Optional
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Use X | None — NOT Optional[X]
def find_user(user_id: int) -> User | None:
...Pathlib over os.path:
from pathlib import Path
current_file = Path(__file__)
parent_dir = current_file.parent
config_path = parent_dir / "config" / "settings.json"Logging over Print:
import logging
logger = logging.getLogger(__name__)
logger.info("Processing started")
logger.error("Failed to process item", exc_info=True)EAFP over LBYL: Use try/except instead of excessive if checks
try:
value = my_dict[key]
except KeyError:
value = default_valueDependency Injection: Pass objects as arguments instead of creating inside functions
def process_data(data: str, validator: Validator) -> bool:
return validator.validate(data)Immutable by Default: Use frozen dataclasses and Final types
from typing import Final
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
name: str
MAX_SIZE: Final = 100Error Handling with Context:
# Python 3.11+ exception groups and add_note()
try:
risky_operation()
except ValueError as e:
e.add_note("Additional context about the error")
raiseTesting:
- Use
pytestfor comprehensive testing - Implement test-driven development (TDD)
- Write unit tests for individual components
- Use integration tests for system validation
Code Smell Elimination:
- Break down "god objects" into focused components
- Eliminate duplicate code (DRY principle)
- Replace "magic numbers" with named constants
- Flatten nested conditionals using
any()/all() - Refactor "long methods" into smaller functions
Common Pitfalls to Avoid:
- Floating-point comparisons (use
math.isclose()) - Mutable default arguments (use
None) - Variable scope issues in loops/lambdas
- Broad exception catching (catch specific exceptions)
- Simplicity First — Choose the simplest solution that works
- Readability Matters — Code should be self-documenting
- Test Thoroughly — Comprehensive testing prevents regressions
- Stay Current — Adopt modern Python features for cleaner code
- Design Patterns — Use patterns strategically, not as golden hammers