Python / Maintainable systems / Lesson 18
Maintainable packages, safe boundaries, and APIs
Design a small stable public API, package it deliberately, and treat external data and process execution as explicit trust boundaries.
On this page
Lesson goals
- A public API is a promise
- Maintain trust through releases
A public API is a promise
A public API specifies supported calls, accepted values, returned structures, mutation, errors, and compatibility. Export a small useful surface and keep implementation details internal so refactoring does not force every caller to change. Keyword-only options and clear names make future extension easier than a long chain of ambiguous positional arguments. Return domain values rather than formatted terminal output when callers may want reuse. Document how invalid input is rejected and whether a failed operation can leave partial effects. A version number is only useful when accompanied by an honest compatibility policy, release notes, and tests for the behavior users rely on.
Packaging records build and project facts
A pyproject.toml can declare the build backend, project metadata, Python compatibility, and dependency requirements under the relevant standards. A source layout separates importable code from repository tooling and helps reveal accidental imports from the working tree during tests. Distribution metadata and import package names are distinct concepts. Build and install a local artifact in a clean environment when reviewing a release, then run tests against the installed code rather than assuming repository imports prove packaging correctness. Building may require an approved backend dependency; the runnable examples here remain standard-library-only and neither install packages nor publish artifacts. Publishing is a separate authorized release action.
Validate a narrow data boundary
The first example validates an already parsed order record and rejects extra fields rather than silently accepting misspellings. type(quantity) is int deliberately rejects bool, because bool is an int subclass and a Boolean quantity is not meaningful under this contract. The parser constructs a new result instead of retaining the caller’s dictionary. Real boundaries also need size limits and authorization appropriate to their operations. JSON is a data format, whereas eval executes Python expressions and pickle loading can execute code from a crafted payload. Never use those execution-capable mechanisms to interpret untrusted input. Safer parsing still requires semantic validation afterward.
def parse_order(data):
if not isinstance(data, dict) or set(data) != {"quantity"}:
raise ValueError("expected quantity field only")
quantity = data["quantity"]
if type(quantity) is not int or quantity < 0:
raise ValueError("quantity must be a nonnegative integer")
return {"quantity": quantity}
print(parse_order({"quantity": 3}))
try:
parse_order({"quantity": True})
except ValueError as error:
print(error)
{'quantity': 3}
quantity must be a nonnegative integer
The explicit boundary accepts three, rejects a Boolean, and returns a newly constructed record rather than trusting arbitrary parsed data.
Parameterize queries and constrain execution
A database query should separate SQL structure from values using the driver’s parameter binding. Do not assemble quoted SQL fragments with string concatenation or f-strings. The second example uses an in-memory SQLite database and a parameterized lookup for a name containing an apostrophe. Bound values are treated as data; this does not authorize the query or parameterize arbitrary table names. For operating-system processes, prefer a fixed executable and an argument list, with shell=False unless a carefully justified shell boundary is required. Avoid executing untrusted command text, bound time and output, and keep secrets out of arguments and logs.
import sqlite3
connection = sqlite3.connect(":memory:")
try:
connection.execute("CREATE TABLE people (name TEXT NOT NULL)")
connection.execute("INSERT INTO people (name) VALUES (?)", ("O'Neil",))
row = connection.execute(
"SELECT name FROM people WHERE name = ?", ("O'Neil",)
).fetchone()
print(row[0])
finally:
connection.close()
O'Neil
Both insertion and lookup bind the name as a value, so its apostrophe does not alter SQL syntax. The database exists only in memory and closes explicitly.
Maintain trust through releases
Review dependencies for necessity, provenance, supported versions, and maintenance before adding them. Record application environments reproducibly while allowing a reusable library’s supported dependency ranges to be tested honestly. Keep credentials outside source and package data, and avoid logging sensitive records when reporting failures. A release checklist should connect tests, supported Python versions, documented APIs, and the contents of built artifacts. Security is ongoing boundary design rather than a decorator added at the end. A maintainable package lets a reader locate the parser, domain calculation, persistence adapter, and entry point without tracing hidden imports or global state across the entire codebase.
Common errors and fixes
A Boolean passes integer quantity validation
- Why it happens
- isinstance(value, int) also accepts bool.
- How to fix it
- Use the exact-type rule when the domain excludes Booleans.
- Prevent it next time
- Include True and False in boundary-validation cases.
A name containing an apostrophe breaks SQL
- Why it happens
- User values were concatenated into SQL syntax.
- How to fix it
- Use parameter binding for values with the driver’s supported syntax.
- Prevent it next time
- Keep query structure fixed and test punctuation-containing values.
The installed package misses a required module
- Why it happens
- Tests imported from the repository instead of the built artifact.
- How to fix it
- Test installation of the local artifact in a clean environment.
- Prevent it next time
- Review artifact contents and supported-version imports before release.
Check your understanding
1. What belongs in a public parser contract for a small order object?
Show worked solution
Specify required and extra fields, accepted exact types, ranges, normalization, size limits, returned ownership, and error behavior. Then test accepted values and each rejection class independently. A type annotation alone cannot enforce any of the external trust assumptions.
2. Why is replacing pickle with JSON useful but insufficient at an untrusted boundary?
Show worked solution
JSON parsing avoids pickle’s object-execution behavior, but valid JSON can still be oversized, structurally wrong, unauthorized, or semantically invalid. Apply limits, schema and domain validation, and authorization before performing effects. Data syntax and permission to act are separate guarantees.
Key takeaways
- A stable API includes errors, ownership, and compatibility.
- Test the built package as well as source behavior.
- Separate data from executable instructions at every boundary.