Data Structures, Abstract Data Types, and Invariants
Objective
Start with “which operations must this support?” rather than “how many containers can I remember.” Distinguish data, Abstract Data Types (ADTs), concrete representations, and algorithms, then use invariants to prove a structure remains valid after an update.
Keep four layers separate
| Layer | Question it answers | Task-scheduling example |
|---|---|---|
| Data element | What is stored? | Task ID, priority, dependency ID |
| ADT / interface | What is allowed? | Add a task, take the next ready task, query status |
| Data structure / representation | How is data organized? | dict index, set dependencies, heapq ready heap |
| Algorithm | How is one operation completed? | Topological progression, heap push/pop, cycle detection |
A queue ADT specifies enqueue and FIFO removal. It can be implemented by collections.deque, a persistent messaging system, or another structure. The interface is the requirement; the representation is a solution. Write the interface and constraints first to know which operation costs matter.
Choose from an operation profile
Before selecting, state:
- data scale and growth pattern;
- the most frequent reads, writes, deletions, and traversals;
- whether order matters, duplicates are allowed, and access is by key or priority;
- whether in-place mutation is allowed and who owns the object;
- capacity, persistence, concurrency, and failure requirements; and
- whether results must be deterministic and replayable.
For the same message batch, list is natural when append and whole-batch traversal dominate; deque(maxlen=100) suits only the most recent 100 messages; frequent lookup by message ID calls for a dict index. There is no best data structure apart from its operations.
Invariants are rules that always hold
An invariant is a condition that must be true after a structure is built and after every public operation completes. For example, a tool registry could require:
- every tool name is unique and non-empty;
- each dictionary key matches the
namewithin its tool object; - every write tool has an explicit approval level; and
- removal leaves no entry in a risk-group auxiliary index.
When maintaining both a list and set:
class OrderedUniqueIds:
def __init__(self) -> None:
self._items: list[str] = []
self._seen: set[str] = set()
def add(self, item: str) -> bool:
if item in self._seen:
return False
self._seen.add(item)
self._items.append(item)
return True
def snapshot(self) -> tuple[str, ...]:
assert len(self._items) == len(self._seen)
assert set(self._items) == self._seen
return tuple(self._items)One method updates both structures, so callers cannot alter only one. The assert statements express internal development-time invariants. Untrusted input still needs ordinary validation and exceptions: optimization mode can remove assertions.
Preconditions, postconditions, and ownership
- Precondition: true before a call, such as a task ID passing format validation.
- Postcondition: guaranteed after success, such as finding the new task by ID.
- Ownership: who may mutate an object and whether a function copies input.
- Failure atomicity: whether failure leaves the structure unchanged or can partially update it.
add_task(task)
Precondition: task.id is non-empty; dependency format is valid
After success: task count +1; by_id[id] refers to the task; no execution before ready
After failure: every index remains as it was before the callThis is closer to a verifiable engineering contract than “it uses a dictionary, so it is fast.”
Abstraction does not mean over-encapsulation
A small script may use dict and set directly. Introduce a class or module boundary only when multiple locations must maintain invariants together, an implementation must be replaceable, or side effects need control. Do not wrap every list, and do not let callers freely mutate several public containers.
Exercise: ADT card
Create a card for an Agent tool registry:
- List
register,get,remove, andlist_by_risk. - Write each operation’s precondition, success postcondition, and failure behavior.
- State at least three global invariants.
- Compare list-only, dictionary-only, and dictionary-plus-group-index representations.
- Explain which operation determines the final choice.
Self-check
- Are a queue ADT and
dequethe same concept? - Why list operations before comparing complexity?
- Which invariant is easiest to break when a list and set are maintained together?
- Why cannot
assertvalidate untrusted input? - What must failure atomicity answer?
Next step and related concepts
- Next: Complexity, Resource Costs, and Measurement.
- See Python Fundamentals for functions, exceptions, and testing boundaries.
References
Retrieved on 2026-07-14.