Why Good Design Starts With Knowing What Should Never Change

Kai Nguyen

Hatched by Kai Nguyen

Jul 28, 2026

10 min read

88%

0

The Hidden Question Behind Clean Code

What if the hardest part of software design is not deciding what to add, but deciding what to separate?

That sounds almost too simple, yet it points to a deep truth about object oriented design in Python. A class is often introduced as a convenient way to bundle data and behavior together. But the real challenge begins after that first bundle: when should a class stay small and focused, when should it inherit from another, and when should it be split into several parts that cooperate instead of one that tries to do everything?

The tension is this: software feels easiest to write when one object knows everything about itself, but software feels easiest to maintain when each object knows almost nothing beyond its own job. That tension is where SOLID principles and Python object oriented programming meet. One gives you the raw machinery of classes, attributes, methods, and inheritance. The other gives you a discipline for using that machinery without turning your codebase into a pile of fragile assumptions.

The deeper idea is not just that code should be organized. It is that good design is an act of boundary making. You are constantly deciding where one responsibility ends and another begins, what should vary and what should remain stable, and which parts of a system deserve to be replaced without disturbing the rest.


A Class Is Not a Thing, It Is a Boundary

In Python, a class is a blueprint, and an instance is the concrete object built from it. That distinction is more than terminology. A class is not merely a container for methods and attributes, it is a statement about what belongs together.

Consider a simple User class. It might store a name, an email address, and a method for printing a friendly representation through __str__(). That seems harmless, even elegant. But add password hashing, email delivery, role management, billing logic, and audit logging, and the class stops being a blueprint for a user. It becomes a miniature application.

That is where the real danger begins. When a class accumulates too many reasons to change, it loses clarity. If email formatting changes, does the user object need to change? If billing rules change, does the same object need to change? If a class answers yes to too many such questions, the boundary is wrong.

This is why object oriented programming is so often misunderstood. The point is not to put everything inside an object. The point is to create objects with coherent responsibilities. Instance attributes hold state that varies from one object to another. Class attributes hold shared information that stays the same across instances. Instance methods define behavior tied to that object’s state. These distinctions are not just syntax, they are design signals.

A well designed class says: this is what changes here, and this is what does not.

Good design is less about packing behavior into objects and more about drawing the right line around what an object is allowed to care about.

That line is where maintainability begins.


The Real Threat: Not Complexity, But Entanglement

Most code does not become hard to maintain because it is big. It becomes hard to maintain because it is entangled. A class that mixes responsibilities becomes a place where unrelated changes collide. You update one method and accidentally break another because both depend on the same internal assumptions.

This is the practical power behind SOLID principles. They are not a philosophy of purity. They are a set of defenses against accidental coupling. Each principle helps preserve the ability to change one part of a system without forcing changes everywhere else.

Python’s object model makes entanglement especially tempting because it is flexible. You can add attributes dynamically. You can override methods in subclasses. You can mutate objects by default. You can make a class behave almost any way you want. That flexibility is a gift, but it also means discipline matters more, not less.

A mutable object is especially revealing here. If an instance can be changed freely, then every method that touches it is entering into a small contract with every other method. If those methods are not carefully separated, one object can become a hidden dependency graph. The result is code that appears simple from the outside but is volatile inside.

The key insight is that maintainability depends on limiting the blast radius of change. When a class has one clear responsibility, the impact of change stays local. When it has many responsibilities, every edit becomes a gamble.

Think of a kitchen where the sink, stove, refrigerator, and checkout counter are all in one station. Nothing is impossible there, but every task interrupts every other task. Better design is not about more equipment. It is about making sure each station has a purpose.


Inheritance Is a Promise, Not Just a Shortcut

Inheritance often looks like the elegant answer to code reuse. A child class can take on the attributes and methods of a parent class, then override or extend what it needs. In a small example, this feels natural. You create a Vehicle class, then Car and Bike inherit from it. Shared behavior lives in the parent, specialized behavior lives in the child. Clean, simple, intuitive.

But inheritance carries a hidden cost: it creates a promise of substitutability. If a child class inherits from a parent, it should behave in a way that respects the parent’s contract. Otherwise, code that expects the parent may fail in surprising ways when given the child.

This is where many designs quietly break down. A subclass is not just a container for shared code. It is a claim that the child is genuinely a specialized form of the parent. If that claim is false, inheritance becomes a trap disguised as reuse.

A useful mental model is to ask: does this child class represent a true is a relationship, or does it merely share some behavior? If the answer is only behavior, composition may be healthier than inheritance. Instead of saying “a car is a vehicle” and forcing everything through one hierarchy, you might create separate objects for engine behavior, display behavior, and navigation behavior, then combine them.

This matters because good design is not about reducing lines of code. It is about reducing the number of assumptions one part of the system makes about another. Inheritance often increases those assumptions. Composition often reduces them.

That does not mean inheritance is bad. It means inheritance is a high trust relationship. Use it when the contract is stable and truly hierarchical. Avoid it when the real relationship is just shared capability.

Inheritance is powerful because it shares code, but dangerous because it also shares expectations.


The SOLID Mindset as a Method for Preserving Freedom

The best way to understand SOLID is not as five separate rules, but as one larger strategy: preserve the freedom to change your mind later.

That is the heart of software design. Every class, method, and attribute should make future changes easier rather than harder. If a class handles one responsibility, it can evolve without dragging unrelated concerns with it. If a function or object depends on abstractions rather than concrete details, it can accept new behaviors without being rewritten. If subclasses truly honor their parent contracts, they can extend behavior without destabilizing it.

Python encourages a pragmatic style, which is one reason it is so effective. You do not need elaborate ceremony to define classes, store instance data in __init__(), use class attributes for shared values, or override __str__() to improve readability. But simplicity at the syntax level is not the same as simplicity at the design level. A small class can still be badly designed. A short method can still hide too many responsibilities. A neat inheritance tree can still create brittle coupling.

The challenge is to design for localized change.

Here is a concrete example. Suppose you build a Report class that both calculates totals and formats output for a console. At first, this seems efficient. But later, you want the same data in a web page, a PDF, and a CSV file. Now your class must know about every output style. The class was built as if formatting were part of reporting, but it turns out they are separate concerns.

A better design might keep Report focused on the data and calculation, while separate formatter objects handle presentation. Now when presentation changes, the calculation object remains untouched. That is not just cleaner architecture. It is a way of protecting the system from unnecessary ripple effects.

This is where class and instance attributes also become more than a Python detail. A class attribute signals shared, stable information. An instance attribute signals localized variation. If you confuse the two, you blur the boundary between what is fixed and what is variable. Good design depends on knowing which is which.


A Practical Framework: Ask Three Boundary Questions

If you want to make these ideas useful in day to day Python work, stop asking only “What class should I create?” and start asking three boundary questions.

1. What is the one thing this object should be responsible for?

If you cannot answer this in one sentence, the class is probably trying to do too much. Responsibility is not a list. It is a focus.

2. What should be allowed to change without forcing this object to change?

This is the most important design question. If you expect formatting, persistence, validation, or delivery to vary over time, do not bury them inside the core object unless they truly belong there.

3. Is inheritance expressing a real relationship, or just convenience?

If the subclass depends on parent details too tightly, or if it only shares a few behaviors, favor composition. Reuse is appealing, but clarity lasts longer.

These questions lead to a powerful habit: design objects around stable core ideas and isolate volatile concerns. Put the things that are truly the same into class attributes or parent classes only when appropriate. Put the things that vary into instance attributes or separate collaborating objects. Keep methods narrowly focused, and use __str__() or other special methods as presentation layers, not as places to sneak in business logic.

The result is not only better code. It is better thinking. You begin to see programs as systems of responsibilities rather than piles of functions and data.


Key Takeaways

  • Define responsibility before writing code. If you cannot say what a class is for in a single clear sentence, it is probably too broad.
  • Treat change as the real enemy. Design objects so that one kind of change does not trigger unrelated edits elsewhere.
  • Use inheritance sparingly and intentionally. A subclass should honor the parent’s contract, not merely borrow its code.
  • Separate stable shared state from variable instance state. Use class attributes for values that truly belong to the class as a whole, and instance attributes for values that differ from object to object.
  • Prefer small, cooperative objects over large, clever ones. Maintainable systems are built from clear boundaries, not from objects that try to do everything.

The Deeper Reframe: Design Is About Protecting Meaning

The most useful way to think about object oriented design is not that it helps you organize code, but that it helps you protect meaning. A class should mean something specific. An instance should represent a concrete example of that meaning. Inheritance should preserve meaning across related types. Methods should make that meaning usable without leaking unrelated concerns.

When design is poor, meaning dissolves. A class named User becomes a place where billing, authentication, email, and formatting all live together. A subclass no longer feels like a specialization, only a workaround. A mutable object becomes a source of surprise because nothing in it has a clear boundary. The code still runs, but the model underneath becomes harder and harder to trust.

That is why the deepest lesson in Python object oriented programming is not about syntax, and the deepest lesson in SOLID is not about rules. Both are about preserving the ability to understand and change a system without fear.

If you remember only one thing, let it be this: good design is not the art of adding more structure. It is the art of deciding what structure must remain stable so everything else can evolve.

When you know what should never change, you finally know where to let the rest of the system breathe.

Sources

← Back to Library

Hatch New Ideas with Glasp AI 🐣

Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)

Start Hatching 🐣