Chapter 20Lesson 01180–240 min

Writing Python Test Libraries and the Library API: Core Concepts and Mental Model

Understand Robot Framework static Python libraries as a deliberate adapter boundary: import, object lifetime, keyword exposure, type conversion, logging, return values, exceptions, secrets, and result evidence.

Robot Framework 7.4.2Static library API@library / @keywordType conversionPublic robot.api

Learning objectives

  • Explain the static Python library call path from Robot import to Python instance, keyword metadata, converted arguments, return/error/logging, and Robot result.
  • Separate Robot variable scope from Python library-instance scope and choose TEST, SUITE, or GLOBAL deliberately.
  • Use @library and @keyword as an explicit public automation API instead of exposing every Python helper.
  • Distinguish runtime argument conversion from return-type documentation metadata.
  • Use public robot.api logging, exceptions, and Secret types without coupling domain logic to Robot internals.

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. The problem: Robot keywords are not the right language for every kind of logic

Chapters 01–19 built increasingly capable Robot suites and tasks. User keywords are excellent for readable orchestration, but they become a poor fit when a workflow needs reusable algorithms, complex validation, protocol adapters, data structures, careful exception mapping, or code that should also be unit-tested outside Robot Framework. At that point, adding deeper IF/FOR/TRY trees can create a second programming language inside the test suite.

A Python test library is the intended escape hatch. Robot remains the readable orchestration surface; Python owns implementation details that are naturally expressed, type-checked, and unit-tested as Python. The engineering goal is not “move everything to Python.” It is to define a small stable boundary between business-facing Robot keywords and domain code.

Boundary rule. A library method may be technically callable from Robot and still be a bad keyword. Expose operations that make sense to automation authors; keep parsing helpers, caches, raw client objects, and internal convenience methods private to Python.

2. Read-only inspection before writing a library

# Bash/zsh or PowerShell commands are identical here.
python --version
python -m robot --version
python -m robot.libdoc --help

# Show where Python would import the framework from.
python -c "import robot; print(robot.__version__); print(robot.__file__)"

# Optional source-level inspection from Python.
python -c "from robot.api import logger, Failure, SkipExecution; print(logger.__name__)"

These commands prove which interpreter owns Robot Framework, whether Libdoc belongs to that same environment, and whether public robot.api symbols resolve. A library that works only because an editor uses a different Python interpreter is not reproducible automation.

3. Mental model: Robot calls an adapter object, not “Python in the suite”

Static Python library execution path
flowchart TD
A[Library setting] --> B[Python module/class import]
B --> C[Library object instance]
C --> D[Keyword metadata / signature]
D --> E[Robot argument conversion]
E --> F[Python method]
F --> G[Domain logic / external adapter]
G --> H[Return value or exception]
F --> I[robot.api.logger]
H --> J[Robot keyword status/result]
I --> J
J --> K[output.xml / log.html / report.html]
C -. lifetime .-> L[TEST / SUITE / GLOBAL scope]
M[Robot variable scope] -. separate .-> E

The Library setting causes Python import and library discovery. For a class library, Robot creates an object according to its library scope. Robot inspects exposed keyword metadata and the Python signature, converts supported arguments, then invokes the method. That method can call pure domain logic or an external client. Return values flow back to Robot unchanged as Python objects; failures are represented by exceptions; log messages can be emitted through the public logging API. Robot records the resulting keyword/test evidence.

The dashed arrows show two lifetimes that must not be confused. Library scope controls how long the Python object lives. Robot variable scope controls where Robot variables are visible. A suite variable does not change a Python library from TEST to SUITE scope, and a GLOBAL library does not automatically create Robot global variables.

4. Define state and ownership before the first mutable method

State store Example Lifetime / owner Primary risk
Robot variable scope ${ORDER_ID} Local/test/suite/global Robot execution scope Assuming it controls Python object lifetime
Python library instance self._quoted_skus TEST, SUITE, or GLOBAL library scope Cross-test leakage when scope is too broad
Python module state module-level cache Process/global for a module library Implicit shared mutable state
Domain object state dataclass/client/cache Owned by Python code Leaking implementation objects into tests
External-system state file/API/database/browser Independent system lifecycle Cleanup/idempotency/credentials
Result evidence output.xml/log/report Robot output directory Sensitive arguments/logs or missing failure context
Secret object Secret wrapper Python object accessible to code Mistaking masking for encryption

A maintainable library documents which of these it reads and mutates. If an object is stateful, scope is part of the public contract—not an implementation footnote.

5. Static library API: module or class maps Python callables to keywords

The static API is the simplest Robot extension model. A module library exposes functions; a class library exposes methods on instantiated objects. By default, public functions/methods can become keywords while names beginning with an underscore are excluded. That automatic discovery can be surprising when base classes or imported functions enter the namespace.

For new class libraries, the explicit pattern is clearer:

from robot.api.deco import keyword, library

@library(scope="TEST", version="1.0.0")
class ExampleLibrary:
    @keyword("Business Action")
    def business_action(self, value: str) -> str:
        return value.upper()

    def helper_not_exposed(self, value: str) -> str:
        return value.strip()

@library sets ROBOT_AUTO_KEYWORDS=False by default. That means the un-decorated public helper above is not a Robot keyword. This makes the automation API reviewable: keyword exposure is a deliberate decision. Since Robot Framework 7.2, a module containing exactly one class decorated with @library can also select that class as the library even when the class name differs from the module name.

6. Library scope is Python object lifetime

Scope Object lifetime Use when Risk
TEST (default) New instance for every test/task; setup/teardown use their own instance State must not cross test boundaries More initialization cost for expensive clients
SUITE One instance per suite A safe suite-owned resource must be shared Tests can accidentally depend on earlier tests
GLOBAL One instance for the whole Robot execution Truly global/stateless/explicitly resettable service Largest blast radius for mutable state
Module library Always global Mostly stateless function collection Module globals become shared state by construction

If a stateful library uses SUITE or GLOBAL scope, provide an explicit reset/close keyword and define who calls it. Parallel execution later makes broad mutable scope even more dangerous because worker processes and external resources add more lifetime boundaries.

7. Argument conversion is executable behavior; return annotations are documentation

Robot data cells are strings unless variables already hold Python objects. For supported type hints, Robot converts arguments before calling the Python method. A method such as def quote(price: float, quantity: int) receives a Python float and int when the cells can be converted. Conversion failure stops the keyword before the body is entered.

Types can come from annotations, @keyword(types=...), supported default values, or library-level custom converters. Use annotations first because Python tooling and Libdoc both understand them.

Important 7.4.2 distinction. def keyword() -> int and @keyword(types={"return": int}) make the return type visible to Libdoc, but Robot Framework does not convert or validate the returned object from that metadata during execution. The Python implementation owns the return-value contract.

8. Failures, errors, skips, and fatal stops are separate contracts

Python behavior Robot meaning When appropriate
AssertionError or robot.api.Failure Normal keyword/test failure System/domain result violated an asserted contract
robot.api.Error / RuntimeError Execution/usage error Library used incorrectly or cannot execute as requested
robot.api.ContinuableFailure Failure but execution may continue Rare cases where collecting more failures is intentional
robot.api.SkipExecution Current test/task is skipped Known condition makes execution inapplicable
robot.api.FatalError Stops whole execution after failure Environment is so invalid remaining tests are meaningless

Do not use logger.error() as a substitute for raising a failure. Logging and status are different channels. A library should emit context with the logger and use an exception when the keyword contract has failed.

9. Public logging API: informative, timestamped, and privacy-aware

from robot.api import logger is the stable logging surface for libraries. It supports TRACE, DEBUG, INFO, WARN, and ERROR plus console/HTML options. When used outside Robot execution, messages route to Python's logging system under the RobotFramework logger, which helps keep domain/unit tests independent.

from robot.api import logger

def reserve(item_id: str) -> None:
    logger.debug(f"Validating item_id={item_id}")
    # Do not log passwords, tokens, raw Authorization headers, or PII.

Logging should report identity and state transitions without copying entire sensitive payloads. The result artifacts are durable operational evidence and therefore part of the security boundary.

10. Secret is a type boundary, not encryption

Robot Framework 7.4 exposes robot.api.types.Secret. A parameter annotated as Secret accepts only Secret objects, helping a library require that callers use Robot's secret-variable mechanism. The string and repr representations hide the underlying value, but Python code can still access secret.value.

from robot.api import logger
from robot.api.types import Secret

def login(token: Secret):
    logger.debug(f"Received token object: {token}")   # representation is masked
    real_token = token.value                            # sensitive from this point
    # Never log real_token. Downstream clients can still leak it.

Secret values are not encrypted, and downstream libraries/processes can disclose them. Chapter 23 will cover secret operations in depth; here the library contract is to minimize exposure and never stringify .value into Robot evidence.

11. DevOps connection: custom libraries create a governed automation boundary

A custom library can be versioned, unit-tested, documented with Libdoc, packaged, reviewed, and reused across suites. That enables architectural separation: Robot files state what operational behavior is required, while Python contains how a reusable capability works. CI can run fast Python unit tests before slower Robot integration tests and can archive Libdoc/spec output as an API contract.

The boundary also improves incident response. A failure can be classified as Robot syntax/selection, library import, conversion, domain logic, external-system state, or result/reporting instead of treating “Robot failed” as one undifferentiated layer.

Knowledge check

Does a Robot suite variable make a Python library instance suite-scoped?

Why is @library useful even when no scope/version option is needed?

A method is annotated -> int but returns a string. Will Robot 7.4.2 automatically convert it to int?

When should a library raise Failure instead of only logging ERROR?

Why can a Secret-typed parameter still leak a credential?

12. Summary and bridge

A Python test library is a typed adapter boundary around domain behavior. Its public contract includes keyword exposure, constructor/keyword signatures, argument conversion, return values, exception semantics, logging, library scope, and privacy behavior. Lesson 2 builds that contract from an empty local project and verifies it independently with Python unit tests, Libdoc, and Robot execution.

Next lesson

Writing Python Test Libraries and the Library API: Guided Hands-On Workflow

Continue with Writing Python Test Libraries and the Library API: Guided Hands-On Workflow. 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.