Chapter 20Lesson 04180–240 min

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

Diagnose Python library failures without private APIs, hidden global state, cwd folklore, accidental keyword exposure, secret leakage, ambiguous exceptions, or false assumptions about type conversion.

Scope leakImport side effectsSecret leakagecwd failurePublic API only

Learning objectives

  • Diagnose import, discovery, scope, conversion, exception, logging, and cwd failures as separate layers.
  • Recognize accidental keyword exposure and mutable GLOBAL state before they create order-dependent suites.
  • Prevent secrets from entering Python/Robot logs even when Secret wrappers are used correctly.
  • Avoid private Robot internals and import-time side effects that make libraries version-fragile.
  • Repair the smallest failing layer while preserving Python and Robot evidence.

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. Production diagnostic sequence: preserve evidence, then narrow the layer

  1. Preserve first-failure artifacts: Robot output/log/report, Python traceback, unit-test output, Libdoc output, exact command, environment/version information.
  2. Confirm versions/interpreter: python --version, python -m robot --version, library/package location.
  3. Confirm executed path/selection/data: which suite/test called which keyword with which safe synthetic inputs.
  4. Validate parse/import graph: module path, package __init__.py, explicit --pythonpath or installed version.
  5. Inspect keyword discovery/signature: Libdoc list/show; decorator boundary; argument types/defaults.
  6. Inspect Python object scope/state: TEST/SUITE/GLOBAL and any external clients/caches.
  7. Inspect external state, timing, parallel/CI/container layers only when relevant.
  8. Apply the least destructive correction and rerun the smallest unit/Robot slice into a new evidence directory.

2. Failure: helper methods become keywords accidentally

# BROKEN: no @library boundary, so both public methods can become keywords.
class BillingLibrary:
    def create_invoice(self, customer):
        return self.parse_internal_record(customer)

    def parse_internal_record(self, raw):
        ...

Symptoms: Libdoc contains implementation-only keywords; autocomplete suggests them; tests start depending on internals. Repair with @library plus explicit @keyword, or with ROBOT_AUTO_KEYWORDS=False. Renaming helpers with an underscore can help but is less explicit as a public API policy.

3. Failure: mutable GLOBAL scope makes test order part of the contract

@library(scope="GLOBAL")
class BrokenInventoryLibrary:
    def __init__(self):
        self.items = []

    @keyword
    def remember(self, item: str):
        self.items.append(item)

    @keyword
    def count(self) -> int:
        return len(self.items)

If test B expects zero but runs after test A appended an item, B fails only in the full suite. Rerunning B alone may pass. That is classic shared-state evidence. Repair to TEST scope unless the shared object represents a deliberate resource. If broader scope is genuinely needed, add explicit reset and prove it in lifecycle setup/teardown.

4. Failure: import-time side effects mutate the machine before a keyword runs

# BROKEN: runs during Python import / Libdoc inspection.
from pathlib import Path

CACHE = Path.home() / ".my-real-cache"
CACHE.mkdir(exist_ok=True)
REMOTE_CLIENT = connect_to_service()

Libdoc, editor discovery, dry-run, and test import can now create directories or network connections. Import must be as safe and deterministic as possible. Move resource acquisition to the library constructor or an explicit keyword with a documented owner, and use disposable paths/authorized targets.

5. Failure: Secret wrapper is unwrapped and logged

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

# BROKEN: explicit disclosure.
def authenticate(token: Secret):
    logger.info(f"Using token {token.value}")

The Robot Secret wrapper did its job until the library accessed the plaintext and logged it. Repair by logging only safe metadata such as a synthetic credential ID, never .value. Search output.xml, log.html, stdout/stderr, debug files, screenshots, and subprocess arguments after security-sensitive changes.

6. Failure: generic exceptions provide no useful automation context

# BROKEN
raise Exception("failed")

# Better adapter boundary
try:
    record = domain_lookup(record_id)
except ValueError as exc:
    raise Failure(f"Cannot load synthetic record {record_id}: {exc}") from exc

Preserve identifiers that help reproduce the failure but exclude secrets/PII. Do not catch every exception and replace it with a generic PASS/FAIL string; unexpected exceptions should remain unexpected evidence.

7. Failure: opaque/custom return objects leak implementation details

Robot can carry arbitrary Python objects in scalar variables, but that does not make every object a good public return type. Returning an internal HTTP client, database cursor, ORM session, or mutable cache lets test data couple directly to implementation APIs and lifetimes.

Prefer domain values such as a mapping, immutable identifier, status object with documented properties, or a higher-level follow-up keyword. If a custom object is intentionally public, document lifetime/thread-safety/serialization expectations and test them.

8. Failure: library depends on private Robot internals

# FRAGILE: internal package names are not the supported integration contract.
from robot.running.context import EXECUTION_CONTEXTS
from robot.output import librarylogger

Robot's internal modules can change without the stability promises of robot.api. Use robot.api.logger, robot.api.deco, robot.api.exceptions, robot.api.types, and other documented public APIs when available. Chapter 21 will introduce dynamic/hybrid/Remote extension architecture; that is still not a license to reach into private execution state.

9. Failure: library import works only from one current working directory

# Works by accident from project root:
python -m robot --pythonpath src tests/robot/inventory.robot

# Fails if a CI job changes cwd and "src" no longer points to project source.
cd tests
python -m robot --pythonpath src robot/inventory.robot

The fix is a documented command contract anchored to the project root, an absolute/resolved search path, or an installed packaged dependency. Do not add arbitrary parent directories to PYTHONPATH until the import succeeds; that can shadow unrelated modules and create a different failure later.

10. Failure: module shadowing changes which library imports

A local file named robot.py, json.py, requests.py, or the same name as the custom package can shadow the intended module. Inspect module.__file__ with the same Python interpreter used by Robot. Rename the collision and recreate evidence rather than manipulating sys.path in library source.

python -c "import rf20_domain, robot; print(rf20_domain.__file__); print(robot.__file__)"

11. Failure: logging ERROR is mistaken for a keyword assertion

logger.error() reports a test execution error; it is not the same contract as raising Failure. If the business condition must fail the current keyword/test, raise an appropriate exception. Reserve ERROR logs for execution-level problems that truly belong in Robot's execution-errors channel.

Similarly, do not raise FatalError merely to avoid writing proper cleanup or error handling. Fatal stopping is for conditions that make the remaining execution meaningless.

12. Intentionally broken scope example: interpret the evidence before fixing

# Change exactly one line in the Chapter 20 lab:
@library(scope="GLOBAL", version="1.0.0")
class InventoryLibrary:
    ...
First Test Mutates State
    ${count}=    Quoted Count
    Should Be Equal As Integers    ${count}    0
    Quote Line    SKU-A    1.0    1

Second Test Requires Isolation
    ${count}=    Quoted Count
    Should Be Equal As Integers    ${count}    0

Predict: the first test passes and leaves one SKU in the shared instance; the second test sees count 1 and fails. Preserve that output. Then restore scope="TEST" and rerun into a new directory. The repaired run demonstrates architecture—not retry magic—changed the state lifetime.

13. Performance: initialization cost is only one layer

Measure import time, constructor/resource initialization, Python keyword time, external-system latency, Robot logging/output serialization, Pabot scheduling, CI/container startup, and artifact upload separately. A GLOBAL singleton may reduce constructor cost but increase correctness risk; do not trade determinism for unmeasured speed.

Unit-test pure Python hot paths independently. Robot is an orchestration/integration framework, not a microbenchmark harness.

14. Troubleshooting shortcuts to reject

  • Do not swallow all Python exceptions and return False so Robot stays green.
  • Do not convert every library to GLOBAL scope to “fix” missing state.
  • Do not append random parent paths to PYTHONPATH until imports work.
  • Do not log plaintext Secret values to diagnose authentication.
  • Do not use private Robot internals when a public API exists.
  • Do not delete first-failure output before comparing the repair.
  • Do not use production APIs/files/databases as a library debugging fixture.

Knowledge check

A test passes alone but fails after another test with a non-zero counter. What should you inspect first?

Why is a successful Libdoc generation valuable during import debugging?

What is wrong with logging secret.value at DEBUG because DEBUG is normally hidden?

A library imports robot.running.context to obtain state. What architectural concern should be raised?

Why preserve the GLOBAL-scope failure before fixing to TEST?

15. Summary and bridge

Library failures become manageable when import/discovery, Python scope, conversion, domain logic, status exceptions, logging, secrets, external state, and Robot evidence are treated as separate layers. Lesson 5 combines these ideas in a checkpoint lab that intentionally introduces both a scope leak and a conversion failure, then repairs the public API contract with independent evidence.

Next lesson

Checkpoint Lab — Writing Python Test Libraries and the Library API

Continue with Checkpoint Lab — Writing Python Test Libraries and the Library API. 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.