keepcalculator

06 Numbers and Powers

Python Class Design: Clear Step-by-Step Guide

Card No. 06·50739 · Updated · Standard formula, shown below

This guide shows you how to design Python classes that are easy to read, maintain, and extend. You’ll learn a practical workflow—from naming and attributes to methods and testing—that keeps your code clear and avoids common pitfalls. No special tools are needed beyond a basic Python environment.

What You’ll Need

  • Python 3.6 or newer (f-strings and type hints work best).
  • A text editor or IDE (VS Code, PyCharm, or even Notepad++).
  • Basic familiarity with functions, variables, and importing modules.

Step-by-Step

  1. Define the class’s single responsibility. Write one sentence describing what the class does. If you can’t do it without “and” or “or,” split it into two classes. For example, Invoice handles billing data; InvoicePrinter handles formatting.
  2. Choose a clear, descriptive name. Use CamelCase (e.g., CustomerAccount, not cust_acc). The name should be a noun that reflects its purpose. Avoid generic names like Data or Manager.
  3. Plan attributes in __init__. List the data the object needs to exist. Set them as instance attributes with descriptive names. Use type hints for clarity:
    class Customer:
        def __init__(self, name: str, email: str, age: int = 0):
            self.name = name
            self.email = email
            self.age = age
  4. Validate attributes early. In __init__, check that values are sensible. Raise ValueError with a clear message if not. This prevents bugs later. Example: if age < 0: raise ValueError("Age cannot be negative").
  5. Use properties for computed or protected attributes. If an attribute should be read-only or requires logic when set, use @property and setter. This keeps the interface clean while controlling access.
    @property
    def full_name(self):
        return f"{self.first} {self.last}"
  6. Add methods that operate on the object’s data. Each method should do one thing. Name methods with verbs (e.g., calculate_total, send_invoice). Keep them short—if a method exceeds 10 lines, consider breaking it into helper methods.
  7. Use dunder methods for built-in behavior. Implement __repr__ for debugging, __str__ for user display, and __eq__ if you need value comparison. This makes your class feel native to Python.
    def __repr__(self):
        return f"Customer({self.name!r}, {self.email!r})"
  8. Decide between inheritance and composition. Use inheritance only when there is an “is-a” relationship (e.g., Dog is a Animal). Otherwise, use composition: give the class an instance of another class as an attribute. Composition is more flexible and easier to test.
  9. Keep class variables for shared state. If all instances should share a value (like a counter or constant), put it at class level. Use @classmethod for methods that need the class, and @staticmethod for utility functions that don’t need instance data.
  10. Write tests for your class. Use unittest or pytest to test initialization, method outputs, and edge cases (e.g., empty strings, negative numbers). This ensures your class works as intended and makes future changes safe.
  11. Document with docstrings. Add a docstring to the class and each public method. Explain what it does, parameters, and return values. Keep it concise but complete. Example:
    def calculate_total(self, tax_rate: float) -> float:
        """Return total price including tax."""
  12. Review and refactor. After writing, step back. Are there any duplicate patterns? Could a method be simpler? Does the class do too much? Refactor early to keep clarity.

Common Mistakes

  • Overusing inheritance—creating deep hierarchies that are hard to follow. Prefer composition.
  • Making all attributes public—allowing external code to change internal state freely. Use leading underscore or properties.
  • Giant __init__ with many parameters—if you have more than 5-6, consider grouping related ones into a dataclass or a separate configuration object.
  • Using mutable default arguments—like def __init__(self, items=[]) which shares the list across instances. Use None and create a new list inside.
  • Ignoring type hints—they aid readability and catch errors with tools like mypy.
  • Not implementing __repr__—makes debugging harder because objects show as <__main__.Customer object at 0x...>.

Tips

  • Start with a plain class and add complexity only when needed. Premature abstraction hurts clarity.
  • Use @dataclass for simple data containers—it automatically generates __init__, __repr__, and more.
  • Keep methods small—around 5-10 lines. If longer, extract helper methods.
  • Name boolean methods with is_ or has_ (e.g., is_valid()).
  • Use self._attribute for internal state that shouldn’t be accessed directly.
  • When in doubt, ask: “What would a new developer expect this to do?”

Troubleshooting

  • AttributeError: ‘X’ object has no attribute ‘y’ — Check if you forgot to set self.y in __init__ or misspelled it. Use hasattr() to debug.
  • Objects are not behaving independently — Likely a mutable default argument or a class variable being modified. Check for def __init__(self, lst=[]) and change to lst=None.
  • Changing an attribute doesn’t affect the object — If you used a property without a setter, it’s read-only. Add a setter or use a regular attribute.
  • Inheritance not working as expected — Forgot to call super().__init__() in the child class. Add it at the beginning of the child’s __init__.
  • Tests fail after a small change — Use git diff to see what changed. Write tests that cover edge cases to catch regressions.

Frequently Asked Questions

What is the difference between a class and an instance?

A class is a blueprint; an instance is an actual object created from that blueprint. For example, Customer is the class, and customer1 = Customer("Alice", "a@b.com") is an instance.

Should I use properties or plain attributes?

Use plain attributes for simple data that doesn’t need validation. Use properties when you need to compute a value or control setting (e.g., ensuring age is non-negative).

How do I decide between a method and a function?

If the operation needs access to the object’s data, make it a method. If it works on generic inputs, a standalone function is better.

Can I have multiple classes in one file?

Yes, but for clarity, keep related classes together. If the file gets long, split into modules. Python doesn’t enforce one-class-per-file.

What is a mixin?

A mixin is a class that provides specific functionality to be mixed into other classes via multiple inheritance. It’s not meant to stand alone. Use them sparingly to avoid complexity.

How do I make a class immutable?

Use @dataclass(frozen=True) or override __setattr__ to raise an error. This is useful for value objects like coordinates or money.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →