Writing Python Test Libraries and the Library API: Configuration, Design Patterns, and Trade-Offs
Choose module versus class libraries, explicit decorators, scope, state ownership, exception/logging contracts, type metadata, and packaging/search-path strategies using observable runtime and CI trade-offs.
Learning objectives
- Choose module or class libraries based on state and constructor needs rather than style preference.
- Select TEST, SUITE, or GLOBAL scope by resource ownership and test independence.
- Use decorators to make keyword exposure explicit and signatures useful to Libdoc/editor tooling.
- Choose failure/logging contracts that preserve Robot truthfulness and Python testability.
-
Compare local
--pythonpathdevelopment with a packaged dependency suitable for CI.
Current compatibility baseline — verified 2026-08-31.
Robot Framework 7.4.2 is the stable course baseline
and requires Python 3.8+. The lab uses the documented static Python
library API, robot.api.deco.library,
robot.api.deco.keyword, robot.api.logger,
public exceptions from robot.api,
robot.api.types.Secret, and Libdoc. The
@library decorator disables automatic keyword discovery
by default; keyword methods are therefore exposed deliberately with
@keyword. Function argument annotations participate in
automatic argument conversion. Return annotations and
@keyword(types={"return": ...}) are documentation
metadata in current Robot Framework and do not convert
returned objects during execution. Secret arguments are masked in
Robot representations but are not encrypted and can still be
disclosed by code that accesses .value. The mandatory
path uses only Python, Robot Framework core, and Python's standard
unittest; no browser, API, database, SSH, Pabot, CI
provider, container runtime, paid platform, or production target is
required.
1. Design from the observable contract backward
A library design is good when a Robot author can predict keyword names, arguments, status, side effects, scope, and evidence without reading implementation internals. Start by listing the operations Robot truly needs. Then decide the smallest Python object lifetime and packaging mechanism that makes those operations deterministic.
Do not choose GLOBAL because construction is inconvenient, or choose a module because it uses fewer lines. Those shortcuts change state sharing and import behavior.
2. Module versus class library
| Choice | Strength | Constraint | Use when |
|---|---|---|---|
| Module library | Minimal mapping from functions to keywords | Always GLOBAL; imported functions may become keywords; no constructor args | Stateless function collection with disciplined namespace |
| Class library | Constructor configuration and explicit object state | Must reason about instance scope/reset | Stateful client/adapter or explicit lifecycle needed |
@library class |
Explicit auto-keyword control + decorator configuration |
Requires @keyword for exposed methods by
default
|
Recommended reviewable boundary for new class libraries |
For a module library, __all__, underscore aliases, or
ROBOT_AUTO_KEYWORDS=False can restrict exposure. A
class with @library is often easier to govern because
keyword methods are visibly marked next to implementation.
3. Decorators versus automatic exposure
Automatic discovery is convenient in tiny libraries, but convenience
becomes ambiguity as inheritance, imports, and helper methods grow.
Explicit decorators improve code review: a reviewer can search for
@keyword and see the entire Robot-facing surface.
@keyword can also define a custom keyword name, tags,
and type information. Avoid changing names gratuitously: the Python
method name can remain implementation-oriented while the Robot
keyword name should be stable and domain-oriented.
from robot.api.deco import keyword, library
@library(scope="TEST")
class AccountLibrary:
@keyword("Create Synthetic Account", tags=["domain:account"])
def create_synthetic_account(self, name: str, limit: int = 10) -> dict[str, object]:
...
4. TEST, SUITE, GLOBAL: choose based on ownership, not speed
| Resource/state | Preferred scope | Reason |
|---|---|---|
| Pure stateless formatter/calculator | TEST or module if truly stateless | No need to preserve mutable state |
| Per-test API client/session | TEST | Isolation is part of correctness |
| Suite-owned local fixture connection | SUITE | Fixture lifecycle exactly matches suite ownership |
| Expensive shared browser/service adapter | GLOBAL only with explicit reset/close and isolation design | Cost may justify sharing, but contamination risk is high |
| Cache keyed by mutable test data | Usually TEST | Avoid invisible cross-test coupling |
Scope affects parallelism indirectly. Pabot uses separate processes, so a GLOBAL Python library instance is global only inside one worker process, while the external resource it points to may still be shared across workers. Chapter 24 will make that capacity/state math explicit.
5. Stateless functions versus instance state
Prefer passing values into functions and returning values out when state does not need to persist. Instance state is justified when it represents a real resource lifecycle (connection, session, client) or a deliberate aggregation. Hidden state increases the number of prerequisites needed to understand a keyword.
If state exists, expose verification and cleanup operations. Do not
force Robot tests to inspect self internals; provide a
domain-facing keyword or return value that can prove state.
6. Robot assertions versus Python exceptions
A custom library should not reimplement every Robot assertion. If a
method returns data, Robot can often assert it with
BuiltIn/Collections. If the Python operation itself cannot satisfy
its domain contract, raise a precise Python exception or
robot.api.Failure in the adapter.
Keep pure domain code framework-neutral. A domain function can raise
ValueError; the Robot adapter can translate it to a
concise Failure. Use SkipExecution,
ContinuableFailure, or FatalError only
when their specific execution semantics are actually required.
7. Python logging versus robot.api.logger
robot.api.logger is best for messages
that should belong to the current Robot keyword/result hierarchy. It
provides accurate timestamps while Robot is running and falls back
to Python logging when Robot is not running.
Python logging is appropriate inside
framework-neutral domain modules and reusable clients. The adapter
can configure/bridge behavior if needed, but avoid global logging
configuration at import time. Libraries embedded in larger test
processes should not unexpectedly change the application's root
logger.
8. Types: optimize the call boundary, not just editor hints
Annotations make the keyword contract executable for supported
arguments. Robot conversion errors then name the
argument/type before the library body runs. This improves
diagnostics and removes repetitive int(...)/bool(...)
parsing.
Return annotations improve Libdoc and Python static tooling but are not runtime conversion/validation in 7.4.2. If returning a custom object, document what Robot callers are expected to do with it. Prefer simple values, mappings, sequences, and domain objects with a clear public contract over opaque internal client handles.
9. Secret-typed arguments: narrow acceptance, narrow exposure
Use a Secret annotation when a keyword should accept only Robot
Secret objects. This is stronger than accepting any string and
hoping callers handle it safely. However, a Secret union such as
str | Secret deliberately permits plain strings and
therefore weakens that boundary.
Once a keyword reads .value, treat the result like any
plaintext secret: do not include it in exception messages, logger
calls, subprocess command lines, screenshots, or returned
dictionaries.
10. Local --pythonpath versus packaged dependency
| Approach | Advantages | Risks/requirements | Recommended use |
|---|---|---|---|
--pythonpath src |
Immediate local development, no build/install step | Command must be documented identically in CI | Small repo-local library / early development |
| Editable install | Normal import behavior during development | Environment mutation; build backend/version must be controlled | Package under active development |
| Built wheel with pinned version | Immutable installable artifact, clean CI dependency graph | Requires package build/publish or internal artifact storage | Shared production library across repos |
| Arbitrary cwd/sys.path hack | Fast workaround | Non-reproducible and collision-prone | Reject |
Packaging does not automatically improve architecture. A poorly
scoped global library remains poorly scoped after publishing.
Conversely, --pythonpath is not inherently bad when it
is an explicit documented project-root command rather than a
workstation-specific environment tweak.
11. Keep configuration layers distinct
| Layer | Examples | Do not confuse with |
|---|---|---|
| Robot core |
Library setting, --pythonpath, output options
|
Python package manager |
| Python environment | venv, installed package/wheel versions | Robot variable scope |
| Custom library | @library scope, constructor args, converters | SUT configuration |
| System under automation | API/file/database/browser state | Library instance state |
| Editor/RobotCode | analysis/import paths/profiles | Runtime truth unless command matches |
| CI/container | workspace, environment, artifact retention | Robot library API itself |
12. Worked decision table
| Scenario | Decision | Why / observable effect |
|---|---|---|
| Pure SKU parser used by Python and Robot | Pure domain function + thin TEST-scoped class adapter | Python tests run independently; Robot adapter maps status/logging |
| One database connection intentionally shared across a suite | SUITE class library + explicit close/reset | Instance lifetime matches fixture owner; no cross-suite connection state |
| Global cache only added to make tests faster | Reject; measure first and redesign isolation | Correctness cannot depend on earlier tests warming mutable state |
| Keyword needs integer quantity from table data | quantity: int annotation |
Robot conversion fails before method body with argument context |
| Keyword returns custom client handle | Usually redesign toward domain operation/value | Prevents Robot tests coupling to Python implementation internals |
| Multiple repos consume same mature library | Build/version wheel in controlled registry/artifact source | CI installs explicit version instead of path folklore |
Knowledge check
Why are module libraries a poor fit for mutable per-test state?
Module libraries are always global in a Robot execution, so module-level mutable state is shared across tests unless manually isolated.
What is the strongest reason to prefer @library +
@keyword in a growing class library?
It makes keyword exposure explicit and prevents unrelated public/inherited methods from silently becoming automation API.
Is GLOBAL scope global across all Pabot worker processes?
No. Each worker is a separate process with its own Python instance, while external resources can still be shared and collide.
When should a domain module import
robot.api.Failure?
Usually it should not. Keep domain logic framework-neutral and translate domain exceptions in the Robot adapter unless the domain module itself is intentionally Robot-specific.
Why is a return annotation still valuable if Robot does not convert the return?
Libdoc and Python tooling can document/check the intended contract, but runtime code must still return the promised object/value itself.
13. Summary and next lesson
Library architecture is a set of explicit contracts: public keyword discovery, object scope, state ownership, argument conversion, return semantics, failures, logging, secrets, and import/packaging. Lesson 4 deliberately breaks these contracts and uses a layered diagnostic sequence to repair them without private APIs or broad workarounds.
References and version anchors
- Robot Framework 7.4.2 — Static library API — module/class discovery and keywords
- Robot Framework 7.4.2 — Argument conversion — annotations/defaults/decorator types
- Robot Framework 7.4.2 — Library scope — TEST/SUITE/GLOBAL
- Robot Framework public logging API — logger behavior
- Python packaging guide — packaged dependency boundary
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.