Skip to main content

pytest-live-report

A pytest plugin that writes its HTML report while your tests run, one card per test case, straight into a single self-contained file.

Open report.html in a browser and refresh: finished tests are already there. No Java, no template directory to configure, no "wait for the whole suite to finish" step — and if you hit Ctrl+C, everything that already ran is still in the report.

$ pip install pytest-live-report
$ pytest --live-report-path report.html

That is the whole setup. The plugin registers itself through its pytest11 entry point, so there is no conftest.py edit and no configuration file.

What you get

  • One card per test case, written the moment the case finishes, with the run's start/end time, wall-clock duration, function name, and its docstring as the description.
  • Status filtering and name search on the page, plus a donut chart of passed / failed / skipped so a long run is scannable.
  • The failure traceback is already in the card, so you do not have to scroll the terminal to find what broke.
  • A single HTML file: the stylesheet, the page script and any screenshots you attach are inlined. Mail it, archive it, open it offline — it works.
  • Structured data for scripts: every page carries a machine-readable run manifest, and a JSON record per test case holding its status, timings, error and the logs you wrote — see Reading the report from a script.
  • pytest-xdist support: with -n, workers hand their cards to the controller, which is the only process that writes the file.

Writing content from a test

from pytest_live_report import live_report


def test_login():
    """Verify the login flow."""
    live_report.log("POST /login as admin")
    live_report.log("status", 200, live_report.span_html("OK", bold=True, code=True))

    live_report.case_name("Login flow")           # override the card title
    live_report.case_desc("Covers the redirect")  # override the docstring
    live_report.save_image("screenshots/home.png", caption="after login")

    assert True
Method What it does
live_report.log(*parts) Appends a log line to the current card. *parts are joined with spaces like print; newlines become separate lines; everything is HTML-escaped.
live_report.span_html(text, *, bold=False, code=False) Renders an inline fragment (bold / monospace). Pass the result to live_report.log() to have it embedded as-is. The only entry point you may call outside a test case.
live_report.case_name(text) Overrides the card title. A parametrized suffix is kept: test_login[admin] shows as Login flow[admin].
live_report.case_desc(text) Overrides the card description (default: the test function's docstring).
live_report.save_image(path, caption=None) Inlines an image as a base64 data URI. Raises FileNotFoundError if the path is not an existing file.

Calling any of these outside a test case only emits a warning; it never fails your run. The report system never raises into your tests — a broken report becomes a warning and the report is disabled for that session.

Command line options

Option Meaning
--live-report-path PATH Where to write the report. Relative paths resolve against rootdir. Without this option the plugin does nothing at all.
--live-report-title TITLE Report title (default: pytest report).

How statuses are counted

pytest has more outcomes than the three the report shows, so they are folded in:

Report status Comes from
passed passed
failed failed and error — a broken fixture (setup error) or a failing teardown counts as a failed case
skipped skipped and xfail

A case is written only once all three of its phases are done, so a case that is still running is simply not in the report yet. A case interrupted by Ctrl+C is dropped as well — you never get a half-written card.

pytest-xdist

$ pytest -n 4 --live-report-path report.html

Workers never touch the report file: each worker attaches its finished card to the test report, xdist ships it back, and the controller writes it. The result has the same cards, counts and content as a single-process run.

One difference: cards appear in the order workers finish, not in test order. Sort or group by the nodeid in each card's JSON record if you need a stable order.

Reading the report from a script

Two kinds of JSON blocks are embedded in the page, so you never have to parse HTML:

import json
import re
from pathlib import Path

html = Path("report.html").read_text(encoding="utf-8")

# The run manifest: its presence means the run finished.
manifest = json.loads(
    re.search(r'<script type="application/json" id="rpt-run">(.*?)</script>', html).group(1)
)
print(manifest["counts"])       # {'total': 42, 'passed': 40, 'failed': 1, 'skipped': 1}
print(manifest["exitstatus"])

# One record per test case.
records = [
    json.loads(block)
    for block in re.findall(
        r'<script type="application/json" class="rpt-case-json">(.*?)</script>', html, re.S
    )
]

Each record holds the case's status, duration, started / finished, error, traceback and skip_reason, plus a summary of what you wrote from inside the test:

# live_report.log("POST /login", 200) becomes one text entry; newlines split into more
for entry in records[0]["logs"]:
    print(entry["ts"], entry["text"])   # 11:14:55 POST /login 200

for shot in records[0]["shots"]:
    print(shot["caption"], shot["bytes"], shot["mime"])   # after pay 29696 image/png

logs carries the plain text you passed to live_report.log(), so it needs no HTML unescaping. Lines written with live_report.span_html() additionally carry an html key with the rendered fragment; text stays clean either way. Newlines split into separate entries, so the entry count matches the Log (n) line on the card.

shots carries the caption, byte size and MIME type of each screenshot — enough to check that a screenshot was taken, without inflating the report. The image data itself lives once, in the card's <img src="data:...">.

Per-case records are versioned by their v field, currently 1. Only additive changes happen within a version, so read optional fields defensively:

for entry in records[0].get("logs", []):
    ...

v is bumped only if a field is renamed, removed, or changes meaning.

The manifest is written only when the session finishes normally. If pytest was interrupted, the file has no manifest — that is how the page (and your script) can tell a finished report from a truncated one.

Requirements

  • Python 3.10+
  • pytest 7.4+
  • pytest-xdist is optional: pip install pytest-live-report[xdist]

MIT licensed. Copyright (c) 2026 Vsoapmac.

Found a bug or have a feature request? Please open an issue.

Metadata

Release files for pytest-live-report 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for pytest-live-report 0.2.0
File Interpreter ABI Platform
pytest_live_report-0.2.0-py3-none-any.whl Python 3 none any Details

Release files / pytest_live_report-0.2.0-py3-none-any.whl

Download URL pytest_live_report-0.2.0-py3-none-any.whl
Size 41.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f6c661b0c9b5f784106536bb4c7485f9d8f430dbac93f65e3989c02f3099d321
BLAKE2b-256 checksum
How to use checksums
c2c63ae16c088fef84d6fbeb2d793d085a0d2de512fe77aaf87176caea0ee694
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Release history Release notifications | RSS feed

0.3.0

1 release file

This release

0.2.0 This release

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page