Chapter 20Lesson 03180–240 min

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.

Module vs classTEST / SUITE / GLOBALExceptionsPackagingCI portability

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 --pythonpath development 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?

What is the strongest reason to prefer @library + @keyword in a growing class library?

Is GLOBAL scope global across all Pabot worker processes?

When should a domain module import robot.api.Failure?

Why is a return annotation still valuable if Robot does not convert the return?

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.

Next lesson

Writing Python Test Libraries and the Library API: Diagnostics, Failure Modes, and Production Practices

Continue with Writing Python Test Libraries and the Library API: Diagnostics, Failure Modes, and Production Practices. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

References and version anchors

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.