FreeCampus Python

Document a Public Python API

Write caller-facing module, function, and result contracts, then inspect the interface that help, pydoc, signatures, and docstrings expose.
python-foundations documentation-publishing docstrings api-reference
Open in Colab
  • Level: Beginner
  • Estimated time: 3–4 hours
  • You will learn: Document public Python behavior, inputs, results, side effects, and anticipated failures without duplicating signatures or narrating implementation.
  • Practice in: Google Colab, JupyterLab, or a local museum_quest package

A caller can see a function’s name and signature, yet still not know what its numbers mean, which inputs are valid, whether it mutates anything, or what to do with a failure. A useful public docstring completes that contract and appears where Python users naturally look: editor help, help(), pydoc, and generated reference.

This lesson asks:

  1. Which names are public enough to document as promises?
  2. What does a signature or type annotation already say, and what remains unexplained?
  3. What belongs in module, class, method, property, and function docstrings?
  4. Which exceptions and side effects matter to a caller?
  5. How can you inspect the documentation Python actually exposes?

1. Document the interface a caller is invited to use

The package initializer selects its public entrance:

from .ranking import ExhibitScore, rank_exhibit

__all__ = ["ExhibitScore", "rank_exhibit"]

A caller should not need _score_time_bonus or _validate_count to complete a normal task. Documenting every private helper as if it were stable creates more promises than the project intends to keep.

Public does not mean “every name without an underscore” by magic. Look for evidence:

  • the package exports the name;
  • the README or examples invite callers to use it;
  • tests protect caller-visible behavior;
  • another package or application depends on it; or
  • maintainers explicitly commit to compatibility.

Comments, docstrings, and long-form pages serve different readers:

POINTS_PER_VOTE = 10  # Keep the public score policy visible near the formula.


def rank_exhibit(name: str, votes: int, minutes_open: int) -> "ExhibitScore":
    """Return the score for one named exhibit."""
    ...
  • The comment explains a source-level maintenance decision.
  • The docstring states caller-visible behavior at runtime.
  • A tutorial can guide a new user through a supplied call.
  • An explanation page can defend the scoring policy and trade-offs.
  • API reference can organize signatures and contracts for lookup.

Do not paste the same paragraph into all five places. Decide where each fact has one maintained source and link when a reader needs more context.

The signature, annotation, docstring, tests, and longer pages contribute different evidence about one public promise.

flowchart TD
  A[Public API promise] --> B[Signature and types]
  A --> C[Docstring contract]
  A --> D[Behavioral tests]
  A --> E[Tutorial and how-to]
  A --> F[Design explanation]
  B --> G[Caller understanding]
  C --> G
  D --> H[Checked examples]
  E --> G
  F --> G

Checkpoint: choose the documentation layer

2. Let the summary line state behavior

PEP 257 recommends a short summary that fits on one line. For a function, begin with the action or returned meaning:

def normalize_label(value: str) -> str:
    """Return a display label with outer whitespace removed."""
    return value.strip()

Avoid repeating the signature:

def normalize_label(value: str) -> str:
    """normalize_label(value: str) -> str"""
    return value.strip()

Python and editor tools already inspect the signature. The useful missing fact is what “normalize” means here. It removes outer whitespace; it does not change capitalization, Unicode normalization, or internal spaces.

A multi-line docstring keeps the summary separate:

def score_time_bonus(minutes_open: int) -> int:
    """Return two points for each complete 30-minute period.

    Partial periods do not contribute points. ``minutes_open`` must be
    non-negative and is measured in minutes.
    """
    if minutes_open < 0:
        raise ValueError("minutes_open must be non-negative")
    return minutes_open // 30 * 2

The summary supports indexes and compact help. The body names rounding, validation, and units—facts the implementation alone should not force every caller to reverse engineer.

Types complement domain meaning

Compare what this signature communicates:

def rank_exhibit(
    name: str,
    votes: int,
    minutes_open: int,
) -> ExhibitScore:
    ...

It says:

  • the caller supplies text and two integers;
  • the function returns an ExhibitScore;
  • argument order and names are visible.

It does not say:

  • whether empty names are accepted;
  • whether counts can be negative;
  • whether open time is seconds or minutes;
  • how partial periods behave;
  • whether the function writes a file;
  • which exception communicates invalid data.

Do not duplicate votes: int as “votes (int): an integer.” Add the domain role: “Non-negative visitor vote count.”

3. Write the complete caller contract

The museum function uses a consistent Google-style layout, but the information matters more than the punctuation:

from dataclasses import dataclass


@dataclass(frozen=True)
class ExhibitScore:
    """Store the public result of ranking one exhibit.

    Attributes:
        name: Visitor-facing exhibit name.
        score: Total ranking points.
    """

    name: str
    score: int


def rank_exhibit(name: str, votes: int, minutes_open: int) -> ExhibitScore:
    """Return the score for one named exhibit.

    Each vote contributes 10 points. Each complete 30-minute period contributes
    2 points; a partial period contributes no time points.

    Args:
        name: Non-empty visitor-facing exhibit name.
        votes: Non-negative visitor vote count.
        minutes_open: Non-negative open duration measured in minutes.

    Returns:
        The exhibit name and calculated score as an immutable result.

    Raises:
        ValueError: If the name is blank or either count is negative.

    Examples:
        >>> rank_exhibit("Moon Dial", votes=4, minutes_open=75)
        ExhibitScore(name='Moon Dial', score=44)
    """
    clean_name = name.strip()
    if not clean_name:
        raise ValueError("name cannot be blank")
    if votes < 0 or minutes_open < 0:
        raise ValueError("votes and minutes_open must be non-negative")
    score = votes * 10 + minutes_open // 30 * 2
    return ExhibitScore(name=clean_name, score=score)

Read the contract from the caller’s side:

  • Behavior: one exhibit becomes one result; it does not save or print.
  • Policy: votes and complete time periods contribute named amounts.
  • Inputs: names are cleaned; values have units and non-negative boundaries.
  • Result: an immutable value carries name and score.
  • Failures: anticipated invalid inputs use ValueError with two boundaries.
  • Example: a small deterministic public call illustrates the ordinary path.

Do not list exceptions the function does not deliberately expose. Hardware failure, MemoryError, or a bug in a future dependency is not an anticipated caller contract merely because it is theoretically possible.

Side effects belong in the promise

A function that writes a file must say so:

from pathlib import Path


def save_score(result: ExhibitScore, destination: Path) -> None:
    """Write one exhibit score as UTF-8 text, replacing `destination`.

    Args:
        result: Public result to serialize.
        destination: File to create or replace.

    Raises:
        OSError: If the destination cannot be written.
    """
    destination.write_text(
        f"{result.name}: {result.score}\n",
        encoding="utf-8",
    )

-> None does not communicate replacement, encoding, newline shape, or the anticipated OSError. A caller deciding whether the function is safe needs those facts.

Checkpoint: complete the public contract

4. Document modules, classes, methods, and properties at their level

A module docstring tells a reader what the module groups and which public entrances matter:

"""Rank museum exhibits using explicit votes and open-time inputs.

Use `rank_exhibit` for the public calculation and `ExhibitScore` for its
immutable result. This module performs no file or network I/O.
"""

A class docstring explains what an instance represents and important invariants. It should not copy every method’s documentation.

class ExhibitCatalog:
    """Collect unique exhibits by their visitor-facing name."""

    def add(self, result: ExhibitScore) -> None:
        """Add `result`, raising `ValueError` when its name already exists."""
        ...

    @property
    def count(self) -> int:
        """Return the number of stored exhibits."""
        ...

The method states its change and duplicate-name failure. The property states the meaning of the exposed number. A constructor docstring is useful when creating the object has rules that the class summary does not explain.

Recognize styles without turning them into rival contracts

Projects commonly encode parameters and results with Google, NumPy, or reStructuredText/Sphinx conventions. For example:

def rank_exhibit(name: str, votes: int, minutes_open: int) -> ExhibitScore:
    """Return the score for one named exhibit.

    Parameters
    ----------
    name
        Non-empty visitor-facing exhibit name.
    votes
        Non-negative visitor vote count.
    minutes_open
        Non-negative open duration in minutes.

    Returns
    -------
    ExhibitScore
        Immutable exhibit name and calculated score.
    """
    ...

The syntax differs from Args: and Returns:, but the caller needs the same facts. Choose a house style compatible with the project’s tooling, use it consistently, and review information quality rather than debating section names in isolation.

5. Inspect what Python exposes

A source docstring is not useful merely because it exists. Inspect the caller’s view:

import inspect

print(inspect.signature(rank_exhibit))
print(rank_exhibit.__doc__.splitlines()[0])

Expected output begins:

(name: str, votes: int, minutes_open: int) -> __main__.ExhibitScore
Return the score for one named exhibit.

The exact qualified return name can differ by module context. The important checks are parameter order/names and the summary line.

Use built-in help:

help(rank_exhibit)

From a local installed project, pydoc produces text outside the interpreter:

python -m pydoc museum_quest.rank_exhibit

Or inspect the module:

python -m pydoc museum_quest

pydoc imports the module. If importing starts an interactive prompt, writes a file, reads secrets, or launches the application, documentation generation will trigger those side effects. Unit 10’s if __name__ == "__main__": boundary and Unit 14’s import-safe design matter here.

Diagnose a stale promise

Suppose help says:

Raises:
    ValueError: If votes is negative.

But the implementation now also rejects blank names and negative open time. Passing tests do not repair prose automatically. Choose the source of truth from an explicit public decision:

  1. Confirm intended behavior in tests and release expectations.
  2. Reproduce each boundary.
  3. Update the docstring to match the intended contract.
  4. Update longer reference or how-to recovery that mentions the old behavior.
  5. Rerun help, doctest, pytest, and the documentation build.

Do not change code to accept invalid values merely to preserve stale prose, and do not change prose to excuse an accidental regression. Resolve the contract first.

Checkpoint: inspect and repair exposed documentation

6. Repair docstrings that create false confidence

Implementation narration

def score_time_bonus(minutes_open: int) -> int:
    """Divide minutes_open by 30 with // and multiply the result by 2."""
    return minutes_open // 30 * 2

The sentence will become stale if implementation changes, and it makes the caller reconstruct the policy. Prefer: “Return two points for each complete 30-minute period.”

Impossible certainty

def load_catalog(path: str) -> list[str]:
    """Always load a valid catalog without errors."""
    ...

File paths can be missing, unreadable, or contain invalid data. Document anticipated failure and validation instead of promising away the boundary.

Copying types without meaning

def rank_exhibit(name: str, votes: int, minutes_open: int) -> ExhibitScore:
    """Rank an exhibit.

    Args:
        name: A string.
        votes: An integer.
        minutes_open: An integer.
    """
    ...

Add non-empty visitor-facing name, non-negative vote count, and non-negative minutes. Those facts help a caller choose values.

Leaking a private mechanism

def rank_exhibit(name: str, votes: int, minutes_open: int) -> ExhibitScore:
    """Call `_validate_count`, then `_score_time_bonus`, then create `_Result`."""
    ...

Private helpers can change during a behavior-preserving refactor. The public contract should survive that change.

7. Lab: document and inspect a small public module

Create src/museum_quest/ranking.py with:

  • a module docstring;
  • frozen ExhibitScore with meaningful attribute documentation;
  • the complete rank_exhibit signature and implementation from this lesson;
  • one private scoring helper whose name does not enter public reference.

Create src/museum_quest/__init__.py that exports only the result and public function. Then:

  1. Predict the signature and summary line help() will display.
  2. Inspect inspect.signature(rank_exhibit) and rank_exhibit.__doc__.
  3. Run python -m pydoc museum_quest from the installed project.
  4. Confirm public names appear and the private helper is not presented as the supported entry path.
  5. Call the ordinary case and both failure categories.
  6. Temporarily make the docstring promise that 75 minutes creates three complete periods. Identify the behavior/doc mismatch without editing the test to hide it.
  7. Repair the promise and list every longer page that should be reviewed.
Hint: write the caller table before the prose

Use columns for name, representation/type, domain meaning, ordinary example, boundary, result, side effect, and anticipated exception. Omit facts that do not apply; translate the table into a concise contract.

Reveal a public-interface check
import inspect

from museum_quest import ExhibitScore, rank_exhibit

assert list(inspect.signature(rank_exhibit).parameters) == [
    "name",
    "votes",
    "minutes_open",
]
assert rank_exhibit.__doc__ is not None
assert rank_exhibit.__doc__.splitlines()[0] == (
    "Return the score for one named exhibit."
)
assert rank_exhibit("Moon Dial", 4, 75) == ExhibitScore("Moon Dial", 44)

try:
    rank_exhibit("   ", 4, 75)
except ValueError as error:
    assert str(error) == "name cannot be blank"
else:
    raise AssertionError("blank name was accepted")

Also inspect the actual help()/pydoc output; assertions over one summary line cannot judge the whole contract’s usefulness.

Key points

  • Document the interface callers are invited to depend on, not every private helper.
  • Signatures and types carry structure; docstrings add domain meaning, units, boundaries, side effects, and anticipated failures.
  • Summary lines state behavior rather than copying signatures or code steps.
  • Module, class, method, property, and function docstrings describe their own level of the public interface.
  • help(), inspect, and pydoc reveal the runtime documentation callers see.
  • Resolve stale behavior/documentation disagreements through an explicit public contract, then update every affected source and check.

References

Next: Keep Small Examples Honest with Doctest

Back to top