Code & API Design
Conventions for Python code and public APIs.
14. Private modules, with the public API declared in __init__.py
- Implementation lives in underscore modules:
_formats.py,_models.py,_backends.py. __init__.pyre-exports the public names and declares an explicit__all__, which includes"__version__".- The rule: if a name isn’t in
__all__, it isn’t public API. Great Docs uses__all__to decide what to document, so this convention keeps the API and the documentation in sync. - In Greenwood, every module also has its own
__all__. A new public symbol is added both there and in__init__.py.
15. from __future__ import annotations everywhere, with full annotations
- Every source file in the newer packages starts with it.
- Hints use
X | None(neverOptional), built-in generics, andLiteral[...]for string-choice parameters (for exampletbl_type: Literal["pandas", "polars"]). - Imports needed only for type hints go under
if TYPE_CHECKING:. This avoids import cycles and avoids importing pandas or polars at runtime.
16. Be DataFrame-agnostic
- Users can pass pandas, polars or pyarrow data.
- narwhals reads the input. Great Tables uses its own lazy backend layer with
singledispatchinstead. - pandas and polars are never imported at module top level. They are imported lazily inside functions, and a missing backend raises a helpful “install
mypkg[pl]” message. - Functions that return tables take an output-format argument (
tbl_type=orformat=) and auto-detect a sensible default when it isNone. - Expensive or optional attributes are loaded lazily through a module-level
__getattr__. For example, Great Tables loads its datasets this way, soimport great_tablesdoesn’t need pandas.
17. Keyword-only arguments for options
The first one or two arguments are positional (the data, the main input). Everything after
*is keyword-only:def meta(es, *, method="REML", level=0.95, ...) -> MetaResult: ... def fit(self, surv, *, data=None, by=None, weights=None): ...Defaults can then be added or reordered later without breaking callers.
18. None means “auto-detect”
- An option such as
make_page: bool | None = Nonelets the library choose based on context, for example the rendering environment. Users override it only when they need to.
19. Rich result objects instead of bare tuples or dicts
- Results are frozen dataclasses with a
*Resultnaming convention:MetaResult,BootstrapResult,RMSTResult. - They have a readable, deterministic
__repr__: no memory addresses, no locale-dependent formatting. - Conversion methods (
to_frame(format=...),to_gt(),plot()) let users move straight into tables and plots. - Objects that display in notebooks implement
_repr_html_. Examples: GT tables and Pointblank validation reports.
20. Consistent, discoverable naming
- Related functions share a verb-family prefix, so tab-completion works as documentation:
- Great Tables:
fmt_*,cols_*,tab_*,opt_*,sub_*. - Pointblank:
col_vals_*,rows_*.
- Great Tables:
- Method chaining reads like a sentence:
GT(df).tab_header(...).fmt_number(...).cols_label(...). In Great Tables, each method returns a new object rather than mutating the existing one.
21. Error messages that tell you how to fix the problem
They name the offending argument, show the bad value, and say what is allowed:
raise ValueError(f"Unknown measure {measure!r}. Valid measures: {valid}.")Argument checks happen once, early (for example in
__post_init__), not throughout the code.Warnings use
warnings.warn(..., stacklevel=2), so they point at the user’s own line of code.
22. Library code never prints
- Output goes through return values, reprs and
warnings. Logging is used only in the larger tools. - CLIs, where present, use Click (
@click.group,@click.version_option) and are exposed through[project.scripts].
23. Bundled example datasets
- A
load_dataset(name, ...)function and a way to list the available datasets are provided, so every docstring example and user-guide page runs out of the box. - The data ships as package data and is read with
importlib.resources.files(...).