The Trap of Over-Decomposition Software developers often treat Clean Code as a rigid checklist: small classes, short methods, and abstractions everywhere. However, Arjan Codes warns that applying these rules blindly leads to **over-decomposition**. This creates a design that looks tidy on the surface but hides a "huge monster in the closet." The problem arises when you optimize for smallness rather than **cohesion**. In the initial example, a sales report script used a Protocol and a Container for dependency injection. While these are technically advanced patterns, they served no functional purpose, merely masking a messy 200-line "run" method that handled loading, filtering, and exporting simultaneously. Real clean code isn't about the number of lines; it is about making the reasons for change visible and grouping logic that belongs together. Refactoring the Fake Abstraction To begin the cleanup, we must strip away abstractions that solve imaginary problems. If a container exists just to instantiate one class that is never swapped out, it is dead weight. By deleting the `ReportService` protocol and the `Container` class, we bring the logic back to the `main` function where it can be directly controlled. Next, we introduce a Data Class for configuration. Instead of passing five or six separate arguments through every function, we group them into a cohesive `ReportConfig` object. This allows for sensible defaults—like UTF-8 encoding or comma delimiters—while making the settings explicit and easy to modify in one location. ```python @dataclass(frozen=True) class ReportConfig: country: str = "Netherlands" min_revenue: float = 10.0 allow_negative: bool = False delimiter: str = "," encoding: str = "utf-8" ``` Making the Pipeline Explicit One of the most significant design improvements involves breaking the monolithic run method into a clear, linear pipeline: **Load → Summarize → Export**. By separating these concerns into pure functions, we gain massive flexibility and efficiency. For instance, by moving the loading logic out of the core processing function, we can load the data once and run multiple summaries against it without re-reading the file from the disk. We also introduce a `TypeAlias` for our data structures to improve readability without the overhead of heavy class hierarchies. ```python from typing import TypeAlias Data: TypeAlias = list[dict[str, str]] def load_data(source: Path, config: ReportConfig) -> Data: # File loading logic here return data ``` Modeling Results with Cohesion Instead of letting the summary data float around as raw numbers, we move behavior into the `Summary` object itself. If you need to convert a summary to text or JSON, that logic belongs to the summary, not the exporter. This shift moves the "how" of the report into the object that owns the "what." We also centralize business logic. By creating an internal `is_valid` helper within the summarization function, we isolate the filtering rules (e.g., checking revenue thresholds or refund status) from the aggregation logic. This makes the primary loop significantly cleaner and focuses the `summarize` function on its actual job: calculating totals. ```python def summarize(data: Data, config: ReportConfig) -> Summary: def is_valid(row: dict) -> bool: return float(row["revenue"]) >= config.min_revenue valid_rows = [row for row in data if is_valid(row)] revenue_sum = sum(float(row["revenue"]) for row in valid_rows) return Summary(count=len(valid_rows), revenue_sum=revenue_sum) ``` Practical Examples and Syntax Notes This approach shines when extending the system. To add a JSON export, we simply define an `export_json` function. Because our pipeline is explicit, we don't have to touch the loading or summarizing code. We just plug the new exporter into our main execution flow. When using Python for these patterns, utilize `Path` objects from `pathlib` rather than strings to handle file system operations more robustly. Additionally, the `asdict` utility from the `dataclasses` module is perfect for quickly converting your cohesive objects into formats suitable for `json.dumps` while maintaining control over the final output structure. Tips and Gotchas Avoid the temptation to abstract early. Wait until you have at least two or three different implementations before reaching for a Protocol. The most common mistake is "hiding" behavior inside services, which makes debugging difficult. High cohesion means things that change together stay together; it doesn't mean every function has to be three lines long. Focus on visibility and meaningful boundaries to keep your code truly maintainable.
Arjan Codes
People
ArjanCodes (3 mentions) creates content around Python, software design, and code architecture, highlighting topics like refactoring mistakes, function/class usage, and SQL solutions from the "I Made a Classic Refactoring Mistake", "Functions vs Classes", and "Raw SQL, SQL Query Builder, or ORM?" videos.
- Mar 27, 2026
- Oct 3, 2025
- Aug 29, 2025
- Mar 14, 2025
- Jan 10, 2025
Overview of Modern Database Interaction SQLAlchemy transforms how developers interact with databases by acting as a sophisticated bridge between relational tables and Python's object-oriented nature. This tool provides an Object Relational Mapper (ORM) that allows you to manipulate database rows as if they were standard Python objects. By abstracting the complexities of raw SQL, it enables cleaner code, better maintainability, and the ability to switch between database backends like SQLite, MySQL, and PostgreSQL with minimal configuration changes. Prerequisites To follow this guide, you should possess a solid grasp of Python fundamentals, including classes and decorators. Familiarity with basic SQL concepts—such as primary keys, foreign keys, and JOIN operations—is necessary. You will also need a Python environment with the library installed via `pip install sqlalchemy`. Key Libraries & Tools * **SQLAlchemy Core**: The foundation providing the SQL Expression Language and database connectivity. * **SQLAlchemy ORM**: The high-level API that maps Python classes to database tables. * **SessionMaker**: A factory for producing `Session` objects to manage database transactions. * **Mapped & mapped_column**: Type-hinting utilities used in modern SQLAlchemy (2.0+) for defining schema. Code Walkthrough: The Object-Oriented Approach Modern database design favors the ORM approach for its readability. First, define your engine and base class: ```python from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker engine = create_engine("sqlite:///:memory:", echo=True) Session = sessionmaker(bind=engine) class Base(DeclarativeBase): pass class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(unique=True) email: Mapped[str] ``` In this snippet, `DeclarativeBase` serves as the registry for your schema. The `Mapped` type hints allow IDEs to provide better autocompletion. To interact with the data, use a session context manager: ```python with Session() as session: new_user = User(username="dev_expert", email="[email protected]") session.add(new_user) session.commit() ``` Complex Relationships and Data Logic SQLAlchemy shines when handling related data. You can define one-to-many relationships using the `relationship` function and `back_populates` to ensure bidirectional synchronization. For instance, a `User` class might have a `posts` attribute that automatically fetches all related entries from a `Post` table. By adding custom methods to these classes, you can encapsulate business logic—like password hashing or permission checks—directly within the data model. Tips & Gotchas Always utilize context managers for sessions to prevent hanging connections. If you encounter performance bottlenecks, enable `echo=True` in your engine configuration to audit the generated SQL. One common pitfall is forgetting to call `session.commit()`; without it, your changes exist only in memory and will vanish once the session closes.
Apr 5, 2024Overview Software development frequently encounters "cross-cutting concerns"—features like logging, authentication, benchmarking, or analytics that must apply across multiple parts of an application. Implementing these manually often leads to messy code duplication and tight coupling. Python decorators provide a structural solution to this problem. They allow you to wrap additional behavior around a function or class without modifying its internal logic. This guide explores the transition from traditional object-oriented decorator patterns to Python's native, functional syntax, emphasizing how to maintain clean, readable code while expanding functionality. Prerequisites To follow this tutorial, you should have a solid grasp of: - **Python Functions**: Understanding how to define and call functions. - **First-Class Functions**: The concept that functions can be passed as arguments and returned from other functions. - **Basic OOP**: Familiarity with classes, inheritance, and abstract methods. - **Type Hinting**: Basic knowledge of Python's `typing` module (e.g., `Callable`). Key Libraries & Tools - functools: A standard Python library for higher-order functions. We specifically use `wraps` and `partial` to maintain metadata and simplify decorator application. - logging: Python's built-in logging facility used to demonstrate real-world cross-cutting concerns. - time: Specifically `perf_counter`, used for measuring execution duration in benchmarking examples. Code Walkthrough The Functional Foundation In Python, a decorator is essentially a function that takes another function as an argument and returns a new "wrapper" function. This wrapper executes the original function but adds logic before and after the call. ```python from functools import wraps import time def benchmark(func): @wraps(func) def wrapper(*args, **kwargs): start_time = time.perf_counter() value = func(*args, **kwargs) end_time = time.perf_counter() run_time = end_time - start_time print(f"Finished {func.__name__} in {run_time:.4f} secs") return value return wrapper ``` In this snippet, `benchmark` defines an inner `wrapper`. The `*args` and `**kwargs` ensure the decorator can handle any function signature. The `@wraps(func)` line is critical; it copies the original function’s metadata (like its name and docstring) to the wrapper. Decorators with Arguments Sometimes you need to pass data into the decorator itself, such as a specific logger instance. This requires an extra layer of nesting—a function factory that returns the actual decorator. ```python def with_logging(logger): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): logger.info(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper return decorator ``` You apply this using `@with_logging(my_logger)`. Python first calls `with_logging` with your argument, which returns the decorator that then wraps your target function. Simplifying with Partial Application Using `functools.partial` allows you to create "default" versions of decorators with pre-filled arguments, reducing boilerplate at the call site. ```python from functools import partial Pre-configure the logging decorator with a specific logger default_logger = logging.getLogger("app") with_default_logging = partial(with_logging, default_logger) @with_default_logging def my_function(): pass ``` Syntax Notes Python uses the `@` symbol as syntactical sugar. Writing `@benchmark` above a function definition is functionally equivalent to writing `my_func = benchmark(my_func)`. This syntax keeps the decoration logic visible and separate from the core business logic. When using multiple decorators, Python applies them from the bottom up (the one closest to the function definition executes first). Practical Examples - **Benchmarking**: Tracking performance bottlenecks in heavy algorithms without polluting the math logic with timing code. - **Logging/Auditing**: Automatically recording when specific administrative functions are called for security audits. - **Authentication**: Checking user permissions before allowing a web endpoint function to execute. - **Retry Logic**: Wrapping network-dependent functions to automatically retry on failure. Tips & Gotchas - **Metadata Preservation**: Always use `@functools.wraps`. Without it, your decorated function will report its name as "wrapper" instead of its actual name, breaking debugging and introspection tools. - **Avoid Signature Bloat**: Don't use decorators to change the expected input or output types of a function. If a function expects an integer and returns a string, the decorated version should do the same. Changing the signature breaks type checkers and confuses other developers. - **Readability Limits**: Just because you can stack five decorators doesn't mean you should. Deeply nested decorators make it difficult to trace the execution flow. Use them for low-level infrastructure concerns rather than complex business logic.
Jan 27, 2023