CoursePython · Object-Oriented Design, Provider Abstraction, and Persistence · part 45 of 79
Part 45 · Object-Oriented Design, Provider Abstraction, and Persistence

Summary

4 min read·9 Sept 2026

Classes are for state that changes and operations that need it. A class whose constructor stores arguments for a single method is a function in disguise, and Module 3's closures do the job with less. Keep __init__ to validation and assignment, prefix internal attributes by convention, write a __repr__ that identifies the object and contains no secrets, and remember that defining __eq__ without __hash__ makes a class unhashable.

Inheritance couples subclasses to a base class's implementation, hides behaviour across files, and stops fitting as cases multiply. Inherit to declare a contract, not to share code. An abstract base class enforces a contract at construction and requires inheritance. A Protocol describes a shape, is checked structurally, and lets implementations know nothing about your interface, which is usually the better dependency direction. For reuse, compose.

Adapters convert incompatible provider SDKs into one interface expressed in your own types, so provider knowledge stops at the adapter boundary. A registry maps a configured name to a factory, so adding a provider means one new file and one registration line. Dependency injection means passing what an object needs rather than constructing it, assembled once in a composition root. Providers genuinely differ in capability, so declare capabilities explicitly rather than pretending they are interchangeable.

Databases need a connection pool with a bounded size and a pool timeout, one engine per process and short sessions. Parameterize every query, and validate against an allowlist anything that must be part of query structure. Transactions make multi-step writes atomic and must not span network calls. Concurrent writers lose updates silently unless you detect the conflict, and an optimistic version check does that cheaply. Keyset pagination stays fast at any depth where offset pagination degrades. Indexes make reads fast, cost writes, and are defeated by wrapping a column in a function.

The repository pattern applies the same isolation to storage. Business logic depends on a Protocol expressed in domain types and never imports a database driver, which makes tests fast and storage swappable. Redis holds ephemeral shared state, with namespaced keys, a TTL on everything, and the assumption that any key may vanish. Schema changes are versioned code, reviewed before committing, separated from data backfills, and written so old and new application versions can both run during a deployment.

Key takeaways

  • A class earns its keep when the state changes and the methods need it
  • Never put a secret in a __repr__
  • Inherit to declare a contract, compose to reuse code
  • A Protocol lets implementations stay ignorant of your interface, which is the right direction
  • Adapters are where provider knowledge stops
  • Adding a provider should mean one new file and one registration line
  • Construct dependencies at the composition root and pass them inward
  • A pool timeout turns an unbounded hang into a visible failure
  • Parameterize values, allowlist structure
  • Never hold a transaction open across a model call
  • A lost update produces no error, so detect it with a version check
  • Offset pagination degrades with depth, keyset pagination does not
  • Business logic that imports a database driver cannot be tested quickly
  • Every Redis key gets a TTL, and any key may vanish
  • Schema changes are code, reviewed and versioned like any other

Common mistakes to remember

  • Writing a class where a function would do
  • Doing network or file work inside __init__
  • A mutable class attribute shared by every instance
  • Including credentials in a repr or log line
  • Defining __eq__ and forgetting __hash__
  • Using inheritance to share implementation, then overriding around it
  • Opening a database connection per query
  • Omitting pool_timeout and getting an unbounded wait
  • Building SQL with an f-string
  • Interpolating a model-supplied column name into a query
  • Holding a transaction open across a provider call
  • Assuming a read-then-write is safe under concurrency
  • Paginating with a large OFFSET
  • Indexing every column, or none
  • Importing SQLAlchemy in a service module
  • Storing the only copy of something in Redis
  • Setting no TTL, or invalidating the cache before writing the update
  • Changing a schema by hand in production
  • Committing an autogenerated migration without reading it
  • Dropping a column in the same release that stops using it