Untangling Python Spaghetti: A Masterclass in Abstraction and Design

Overview

Writing Python code is easy, but maintaining it as it grows is a different beast entirely. Without a clear architectural strategy, projects quickly devolve into "spaghetti code"—a mess of tight coupling, circular imports, and fragile dependencies. This tutorial demonstrates how to use abstraction to decouple your code, specifically focusing on how to transition from concrete implementations to flexible contracts. By shifting focus from what a specific class is to how it should behave, you create a system that is easier to test, extend, and understand. We will explore three primary ways to implement these contracts:

(ABCs),
Protocols
, and
Callables
.

Prerequisites

To follow this guide, you should have a solid grasp of

fundamentals, including classes, functions, and basic type hinting. Familiarity with the
Pillow
library for image processing and the concept of dependency injection will help you grasp the architectural shifts being made.

Untangling Python Spaghetti: A Masterclass in Abstraction and Design
This Is Why Your Python Code Turns Into Spaghetti

Key Libraries & Tools

  • abc: The built-in module for defining Abstract Base Classes.
  • typing: Contains Protocol for structural typing and Callable for functional abstractions.
  • functools: Specifically the partial function for partial argument application.
  • Pillow (PIL): Used for the underlying image manipulation tasks.

Code Walkthrough

The Problem: Concrete Coupling

In the original "spaghetti" version, the processing function explicitly checks the type of each filter and applies settings based on that type. This requires importing every specific filter class into the processing module.

Solution 1: Abstract Base Classes (ABCs)

By defining a base contract, we ensure every filter implements an apply method. This allows the processor to treat any filter the same way.

from abc import ABC, abstractmethod
from PIL import Image

class FilterBase(ABC):
    @property
    @abstractmethod
    def name(self) -> str:
        pass

    @abstractmethod
    def apply(self, image: Image.Image) -> Image.Image:
        pass

Solution 2: Protocols (Structural Typing)

allow for "duck typing" with static type safety. Unlike ABCs, your filter classes don't need to inherit from the protocol; they just need to have the matching methods.

from typing import Protocol

class Filter(Protocol):
    def apply(self, image: Image.Image) -> Image.Image:
        ...

Solution 3: Callables and Functional Design

Sometimes a class is overkill. We can represent a filter as a simple Callable that takes an image and returns an image. To handle filters that need configuration (like intensity), we use closures or functools.partial.

from functools import partial
from typing import Callable

ImageFilter = Callable[[Image.Image], Image.Image]

def apply_grayscale(image: Image.Image, intensity: float) -> Image.Image:
    # implementation logic
    return image.convert("L")

# Create a configured filter function
grayscale_filter = partial(apply_grayscale, intensity=0.5)

Syntax Notes

When using Protocol, the ... (ellipsis) is the standard way to indicate a method body that exists only for type checking. For Callable, the syntax Callable[[Arg1Type, Arg2Type], ReturnType] provides precise hints for higher-order functions.

Practical Examples

This pattern is essential in plugin architectures. For instance, if you are building a data export tool, you can define an Exporter Protocol. Whether you add

,
JSON
, or
SQL
exports later, your main logic remains untouched because it only interacts with the abstraction.

Tips & Gotchas

Avoid over-engineering; if you only have two filters that never change, abstractions might be unnecessary. Beware that functools.partial objects do not carry the __name__ attribute of the original function, which can break logging or debugging tools that rely on function names. Always designate a "dirty corner" in your code—usually the main.py file—where concrete instances are actually created and wired together.

4 min read