Designing a Clean Python API Under Pressure

The low-level-design round grades your interface, not your diagram. Build one small key-value store live — but design only: derive the API from the call site first, state and enforce the invariants, shape the exception surface, add dunders where they earn their keep, and let the docstring be the spec. Every snippet runs in real CPython.

Concept · Languages & Runtimes. The source ↗

A free, interactive, animated visual explainer of Designing a Clean Python API Under Pressure — built to be understood, not skimmed.

Questions

What makes a Python API "clean" in a low-level-design interview?
A clean API is legible before it is run: the class name, the method names, the signatures, and the invariants tell a reader exactly what the object promises without opening a single method body. In an LLD round the interviewer reads that surface first and grades it — is set(key, value) ordered the way every mapping is, does an absent key raise instead of returning a silent None, is max_bytes validated in __init__ so a bad object can never exist? The trick is to design the surface from the caller inward: write the usage snippet you wish you could write, then give each call site the exact signature it forces. The implementation is the easy part and is graded last.
Should a Python get() return None or raise for a missing key?
For a store whose whole job is holding values, raise. Returning None for a missing key silently conflates "the key is absent" with "the value happens to be None", and it pushes an is-None check onto every caller — the one they forget is the bug. A raise is loud at the exact call site that got it wrong. dict itself splits the two: d[k] raises KeyError, while d.get(k) returns None precisely because you opted into a default. So offer both if you like — a raising __getitem__/get and an explicit get(key, default) — but never make silent None the only behavior. The worked example raises KeyNotFoundError, a subclass of KeyError, so existing except KeyError handlers keep working.
When should a Python class implement dunder methods like __getitem__ or __len__?
When the object genuinely behaves like the thing the syntax is for. A key-value store is a mapping, so store[key], key in store, and len(store) all read naturally — implementing __getitem__, __contains__, and __len__ makes the object work with the syntax and built-ins every Python reader already reaches for, which is real ergonomics. The failure mode is showing off: adding __add__, __call__, or __iter__ that returns something surprising just to look clever. The test is a reader's expectation — if store + other or len(store) would make a teammate pause and ask "what does that even do here?", the dunder is costing clarity, not buying it.
When should you use a property instead of a method in Python?
Use a property when the thing reads like a piece of data the object simply has — store.bytes_used, not store.get_bytes_used() — and the access is cheap. PEP 8 says it directly: "For simple public data attributes, it is best to expose just the attribute name," and when a data attribute needs to grow behavior, "use properties to hide functional implementation behind simple data attribute access syntax." The hard rule is the other half: "Avoid using properties for computationally expensive operations; the attribute notation makes the caller believe that access is (relatively) cheap." A property that quietly does a database call or an O(n) scan on every read is a trap — keep that a method whose parentheses warn the caller work is happening.
Should you write a custom exception or reuse a built-in like KeyError?
Reuse the built-in when your failure IS that built-in — an absent key is a KeyError, a bad argument is a ValueError. Add a custom exception when you need a failure the caller can catch specifically or one that must carry state. The strongest move is both: subclass the built-in. KeyNotFoundError(KeyError) is catchable as either, and its __init__ stores the missing key and builds a message from it, so the exception carries the data a handler needs instead of a bare string. PEP 8: "Derive exceptions from Exception rather than BaseException." Design the failure surface — which exceptions, subclassing what, carrying what — with the same care as the success path; it is part of the API.

Related explainers