Skip to main content
Back to Blog
Guide
2026-05-10

Robot Framework Custom Python Libraries Complete Guide

Build custom Python libraries for Robot Framework. Static, dynamic, hybrid APIs, library scopes, ROBOT_LIBRARY_LISTENER, packaging, and distribution patterns.

Robot Framework Custom Python Libraries Complete Guide

Robot Framework's keyword libraries cover most common testing needs - HTTP, browsers, databases, file system, and more - but every team eventually hits scenarios where a custom library makes more sense than chaining built-in keywords. Maybe you need to interact with a proprietary internal API, hash files with a specific algorithm, parse a custom file format, or compute metrics from multiple data sources. Python is Robot Framework's native extension language, and writing a custom library is straightforward once you understand the conventions.

This complete guide walks through the three library APIs (static, dynamic, hybrid), library scope options (GLOBAL, SUITE, TEST), how to expose Python functions as Robot keywords with named arguments and documentation, the listener interface for reacting to events, packaging your library for distribution via PyPI, and testing the library itself. Examples cover an HTTP wrapper, a custom database utility, an AWS S3 helper, and a stateful workflow library. By the end, you'll be ready to write reusable libraries that make your test suites cleaner and more maintainable.

Key Takeaways

  • Robot Framework can import any Python module as a keyword library
  • The static API exposes top-level functions as keywords
  • The dynamic API generates keywords at runtime
  • Library scope controls instance lifetime (test, suite, or global)
  • Use ROBOT_LIBRARY_DOC_FORMAT and @keyword decorator for rich docs
  • Listeners react to test lifecycle events
  • Publish to PyPI for company-wide sharing

Static Library

The simplest pattern: a Python file with functions becomes a keyword library.

# libraries/MathLibrary.py

def add_numbers(a, b):
    """Returns the sum of two numbers."""
    return float(a) + float(b)

def is_even(number):
    """Returns True if the number is even."""
    return int(number) % 2 == 0
*** Settings ***
Library    libraries/MathLibrary.py

*** Test Cases ***
Basic Math
    ${result}=    Add Numbers    2    3
    Should Be Equal As Numbers    ${result}    5
    ${even}=    Is Even    4
    Should Be True    ${even}

Underscores in Python become spaces in Robot. add_numbers becomes Add Numbers.

Class Based Library

# libraries/Calculator.py

class Calculator:
    """A simple calculator library."""

    def __init__(self):
        self._history = []

    def add(self, a, b):
        """Adds two numbers and stores the result in history."""
        result = float(a) + float(b)
        self._history.append(result)
        return result

    def get_history(self):
        """Returns all calculation results so far."""
        return self._history
*** Settings ***
Library    libraries/Calculator.py

*** Test Cases ***
Calculator Tracks History
    Add    1    2
    Add    3    4
    ${history}=    Get History
    Length Should Be    ${history}    2

Library Scope

Default scope is TEST - a new instance per test. Other options:

class Calculator:
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'  # one instance for entire run
    # or 'SUITE' - one per suite
    # or 'TEST' - one per test
ScopeWhen To Use
GLOBALStateless utilities, expensive setup
SUITEPer-suite state, login session, DB connection
TESTDefault, isolated per test

Keyword Documentation

Use @keyword decorator for rich docstrings and named args:

from robot.api.deco import keyword

class APIClient:
    @keyword('Login User ${user}')
    def login_user(self, user, password='default'):
        """Logs in the specified user.

        Examples:
        | Login User alice |
        | Login User bob password=specialpass |
        """
        # ...

Dynamic API

When you don't know your keywords at design time, use the dynamic API:

class DynamicLibrary:
    def __init__(self):
        self._keywords = {
            'Open URL': self._open_url,
            'Click Button': self._click_button,
        }

    def get_keyword_names(self):
        return list(self._keywords.keys())

    def run_keyword(self, name, args, kwargs):
        return self._keywords[name](*args, **kwargs)

    def _open_url(self, url):
        return f'opening {url}'

    def _click_button(self, label):
        return f'clicking {label}'

This is how libraries like SeleniumLibrary and Browser implement their large keyword sets.

Library Listeners

Listeners receive events:

class DatabaseLibrary:
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'
    ROBOT_LIBRARY_LISTENER = None

    def __init__(self):
        self.ROBOT_LIBRARY_LISTENER = self
        self._connection = None

    def start_test(self, name, attrs):
        # Open per-test connection
        pass

    def end_test(self, name, attrs):
        # Close connection
        pass

    def query(self, sql):
        return self._connection.execute(sql).fetchall()

Real Example: HTTP Wrapper

# libraries/CompanyAPI.py
import requests
from robot.api.deco import keyword

class CompanyAPI:
    ROBOT_LIBRARY_SCOPE = 'SUITE'

    def __init__(self, base_url=None, token=None):
        self.base_url = base_url or 'https://api.example.com'
        self.token = token

    @keyword('Set Auth Token ${token}')
    def set_auth_token(self, token):
        self.token = token

    @keyword('Get User ${user_id}')
    def get_user(self, user_id):
        response = requests.get(
            f'{self.base_url}/users/{user_id}',
            headers={'Authorization': f'Bearer {self.token}'},
        )
        response.raise_for_status()
        return response.json()

    @keyword('Create User ${name} ${email}')
    def create_user(self, name, email):
        response = requests.post(
            f'{self.base_url}/users',
            json={'name': name, 'email': email},
            headers={'Authorization': f'Bearer {self.token}'},
        )
        response.raise_for_status()
        return response.json()['id']

Usage:

*** Settings ***
Library    libraries/CompanyAPI.py    base_url=https://api.example.com

*** Test Cases ***
End To End User Flow
    Set Auth Token    abc123
    ${user_id}=    Create User    Alice    alice@example.com
    ${user}=    Get User    ${user_id}
    Should Be Equal    ${user}[email]    alice@example.com

Real Example: AWS S3 Helper

# libraries/S3Library.py
import boto3
from robot.api.deco import keyword

class S3Library:
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'

    def __init__(self, region='us-east-1'):
        self.client = boto3.client('s3', region_name=region)

    @keyword('Upload File To S3')
    def upload_file_to_s3(self, local_path, bucket, key):
        self.client.upload_file(local_path, bucket, key)

    @keyword('Download File From S3')
    def download_file_from_s3(self, bucket, key, local_path):
        self.client.download_file(bucket, key, local_path)

    @keyword('S3 Object Should Exist')
    def s3_object_should_exist(self, bucket, key):
        try:
            self.client.head_object(Bucket=bucket, Key=key)
        except self.client.exceptions.ClientError:
            raise AssertionError(f'Object {key} does not exist in {bucket}')

Custom Exceptions

Raising clean exceptions improves test reports:

class APIError(Exception):
    ROBOT_CONTINUE_ON_FAILURE = False
    ROBOT_SUPPRESS_NAME = True

class APIClient:
    @keyword
    def get_user(self, user_id):
        response = requests.get(f'.../users/{user_id}')
        if response.status_code == 404:
            raise APIError(f'User {user_id} not found')
        response.raise_for_status()
        return response.json()

ROBOT_CONTINUE_ON_FAILURE=False ensures the test stops on this error.

Variable Imports

Pass arguments when importing:

*** Settings ***
Library    libraries/CompanyAPI.py    base_url=https://api.staging.example.com    token=%{API_TOKEN}
class CompanyAPI:
    def __init__(self, base_url='https://api.example.com', token=None):
        self.base_url = base_url
        self.token = token

Logging From Python

from robot.api import logger

class MyLibrary:
    def do_work(self):
        logger.info('Starting work')
        logger.debug('Detail info')
        logger.warn('Warning message')

These appear in the Robot HTML log.

Returning Multiple Values

def get_user_details(self, user_id):
    """Returns (name, email, role) tuple."""
    user = self._fetch(user_id)
    return user['name'], user['email'], user['role']
${name}    ${email}    ${role}=    Get User Details    42

Packaging For PyPI

my_library/
  pyproject.toml
  README.md
  src/
    my_library/
      __init__.py
      keywords.py
  tests/
# pyproject.toml
[project]
name = "robotframework-mycompany"
version = "1.0.0"
dependencies = ["robotframework>=6.0", "requests"]

[project.optional-dependencies]
test = ["pytest"]

Publish:

python -m build
python -m twine upload dist/*

Versioning

Include a version constant:

class MyLibrary:
    ROBOT_LIBRARY_VERSION = '1.2.3'

Shows in the Robot log header.

Testing Custom Libraries

Use pytest:

# tests/test_calculator.py
from libraries.Calculator import Calculator

def test_add():
    calc = Calculator()
    assert calc.add(2, 3) == 5

def test_history_tracks_calls():
    calc = Calculator()
    calc.add(1, 2)
    calc.add(3, 4)
    assert calc.get_history() == [3.0, 7.0]
pytest tests/

Documenting Libraries

Generate HTML docs:

python -m robot.libdoc libraries/MyLibrary.py docs/MyLibrary.html

This produces searchable documentation with all keywords and arguments.

Library Comparison

APIUse CaseEffort
StaticMost casesLowest
ClassStateful librariesLow
DynamicPlugin systemsMedium
HybridMix of bothMedium

Real Suite Example

*** Settings ***
Documentation    Tests using a custom library
Library          libraries/CompanyAPI.py    base_url=%{API_URL}    token=%{API_TOKEN}
Library          libraries/S3Library.py
Suite Setup      Create Test User
Suite Teardown   Cleanup Test User

*** Variables ***
${TEST_USER_NAME}    test-user-${TIMESTAMP}
${TEST_USER_EMAIL}   test-${TIMESTAMP}@example.com

*** Test Cases ***
User Can Upload Avatar
    [Tags]    smoke    profile
    Upload File To S3    /tmp/avatar.png    avatars    ${TEST_USER_ID}.png
    S3 Object Should Exist    avatars    ${TEST_USER_ID}.png

*** Keywords ***
Create Test User
    ${id}=    Create User    ${TEST_USER_NAME}    ${TEST_USER_EMAIL}
    Set Suite Variable    ${TEST_USER_ID}    ${id}

Cleanup Test User
    Delete User    ${TEST_USER_ID}

Anti-Patterns

Anti-PatternBetter
Long inline logic in robotExtract to Python
One mega libraryPer-domain libraries
Stateful globalsClass with proper scope
Silent errorsRaise meaningful exceptions
No docstringsDocument every keyword

Distribution

Internal teams can host on a private PyPI (Artifactory, AWS CodeArtifact, GitHub Packages). External libraries should target the official PyPI for discovery.

Conclusion

Custom Python libraries are the bridge between Robot Framework's keyword-driven syntax and the rich Python ecosystem. With a few hours of work, you can wrap any internal tool, API, or workflow as Robot keywords that read like English to your team. Apply this pattern judiciously: don't replace built-ins, don't put business logic inside, and keep each library focused on one domain. Done well, custom libraries make Robot test suites read like business specifications while delivering all the power of Python under the hood.

Start by identifying one repeated multi-step keyword sequence in your suite. Extract it to a Python class, expose one keyword, and use it from a single test. As patterns emerge, build out the library. Publish internally so other teams benefit. Explore our skills directory for related patterns or the Python testing guide for broader Python testing.

Robot Framework Custom Python Libraries Complete Guide | QASkills.sh