# https://github.com/beanbaginc/kgb Project Manual

Generated at: 2026-06-14 16:33:45 UTC

## Table of Contents

- [Overview and Quick Start](#page-1)
- [Core Architecture: SpyAgency, FunctionSpy, and SpyCall](#page-2)
- [Spy Operations and Assertions](#page-3)
- [Testing Framework Integration and Platform Compatibility](#page-4)

<a id='page-1'></a>

## Overview and Quick Start

### Related Pages

Related topics: [Core Architecture: SpyAgency, FunctionSpy, and SpyCall](#page-2), [Spy Operations and Assertions](#page-3)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)
- [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)
- [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)
- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)
- [kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py)
- [kgb/utils.py](https://github.com/beanbaginc/kgb/blob/main/kgb/utils.py)
- [kgb/pycompat.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pycompat.py)
</details>

# Overview and Quick Start

## Introduction

`kgb` is a Python function-spying library used to intercept calls to functions, methods, and classmethods in unit tests. It captures arguments, return values, and raised exceptions for later inspection, while still allowing the original implementation to be invoked or replaced with a fake. The library is maintained by Beanbag Inc. and is used in projects such as Review Board, Djblets, and Django Evolution. Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)

The package's public surface is small and focused. The top-level module re-exports the agency, the standalone context manager, and a set of spy operation helpers used to script complex return/raise behavior:

```python
# Source: kgb/__init__.py
__all__ = [
    'SpyAgency',
    'SpyOpMatchAny',
    'SpyOpMatchInOrder',
    'SpyOpRaise',
    'SpyOpRaiseInOrder',
    'SpyOpReturn',
    'SpyOpReturnInOrder',
    'spy_on',
    ...
]
```

Source: [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)

The current development version is `7.3.1 alpha`, with version `7.3` being the most recent stable release line that added Python 3.14 support and a `SpyAgency.get_spy()` helper. Source: [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py), [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)

## Installation and Python Compatibility

Installation is a single `pip` command:

```bash
$ pip install kgb
```

Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)

`kgb` supports both CPython and PyPy. Version `7.0` dropped Python 2.6, 3.4, and 3.5. Subsequent releases added support for newer interpreters, as summarized below.

| Release | Notable Change |
|---------|----------------|
| 7.0     | Dropped legacy Python; introduced `pytest` plugin and snake_case assertions. |
| 7.1     | Added Python 3.11 support. |
| 7.1.1   | Packaged the `LICENSE` file. |
| 7.2     | Added Python 3.13 support; fixed related code-generation crashes. |
| 7.3     | Added Python 3.14 support and `SpyAgency.get_spy()`. |

Source: [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)

The README mentions support for "Python 2.7 and 3.6 through 3.11" in its original text, but the more recent `NEWS.rst` indicates the project has been updated for 3.13 and 3.14. Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst), [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)

## Core Architecture

At a high level, `kgb` works by replacing a target function's code object with a generated forwarder that records each call before delegating either to the original function or to a user-supplied fake. The replacement is set up by `FunctionSpy` and managed by `SpyAgency`.

```mermaid
flowchart LR
    Caller[Caller code] --> Spy["Replaced function\n(FunctionSpy forwarder)"]
    Spy -->|records args| Calls[(Call history)]
    Spy -->|call_original=True| Original[Original function]
    Spy -->|call_fake=fn| Fake[User fake]
    Spy -->|spy_op chain| Ops["SpyOpReturn / SpyOpRaise / SpyOpMatchAny"]
    Agency[SpyAgency] -->|tracks| Spy
    Agency -->|unspy_all| Spy
```

The forwarder is generated from the spied function's signature, which is captured by `FunctionSig` (a `BaseFunctionSig` subclass). Python 3 uses `FunctionSigPy3` and relies on `inspect.Signature` to enumerate parameters, including positional-only and keyword-only arguments. Source: [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)

On Python 3.11 and newer, `kgb` also mirrors `co_flags` bits for generators, coroutines, and async generators from the original code object onto the replacement. This avoids `DeprecationWarning` messages that the interpreter would otherwise emit. Source: [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)

The `SpyAgency` class holds a `set` of registered spies and exposes lifecycle methods. When mixed into a `unittest.TestCase`, its `tearDown` automatically calls `unspy_all`, ensuring spies do not leak between tests. Source: [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)

## Quick Start Examples

`kgb` supports four primary usage patterns. All of them revolve around `spy_on`.

### 1. Manual `SpyAgency` instance

The most explicit form creates an agency, registers a spy, and queries it:

```python
from kgb import SpyAgency

def test_mind_control_device():
    mcd = MindControlDevice()
    agency = SpyAgency()
    agency.spy_on(mcd.assassinate, call_fake=give_hugs)
    ...
```

Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)

### 2. Mixin with `unittest`

`SpyAgency` can be combined with a test base class. Its `tearDown` cleans up automatically:

```python
import unittest
from kgb import SpyAgency

class TopSecretTests(SpyAgency, unittest.TestCase):
    def test_doomsday_device(self):
        dd = DoomsdayDevice()

        @self.spy_for(dd.kaboom)
        def _save_world(*args, **kwargs):
            print('Sprinkles and ponies!')

        dd.kaboom()
```

Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst), [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)

### 3. Pytest fixture

When `kgb` is installed, it registers a `pytest` plugin. The `spy_agency` fixture yields a fresh agency and calls `unspy_all` on teardown:

```python
def test_doomsday_device(spy_agency):
    dd = DoomsdayDevice()

    @spy_agency.spy_for(dd.kaboom)
    def _save_world(*args, **kwargs):
        print('Sprinkles and ponies!')

    dd.kaboom()
```

Source: [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py), [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)

### 4. Standalone context manager

For one-off spies, the top-level `spy_on` is usable as a context manager without instantiating an agency:

```python
from kgb import spy_on

def test_the_bomb():
    bomb = Bomb()
    with spy_on(bomb.explode, call_original=False):
        bomb.explode()  # Won't actually explode.
```

Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)

## Common Operations and Assertions

Once a spy is in place, you can inspect calls, assert behavior, reset history, and invoke the original implementation:

```python
result = obj.function.call_original('foo', bar='baz')
obj.function.reset_calls()
```

Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)

`SpyAgency` exposes camelCase assertion methods (e.g., `assertSpyCalledWith`, `assertSpyRaised`) and, since `7.0`, snake_case aliases (e.g., `assert_spy_called_with`). Source: [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py), [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)

When a spy cannot be mixed in, the same assertions are available as free functions in `kgb.asserts`, which internally uses a module-level `SpyAgency`. Source: [kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py)

A common source of confusion is reusing the same spy across multiple test invocations (e.g., from `ddt`-driven parameter expansion). Re-applying `spy_on` to an already-spied function raises `ExistingSpyError`; the cure is to call `unspy_all` (or the per-spy `unspy()`) between test iterations. Source: [kgb/errors.py](https://github.com/beanbaginc/kgb/blob/main/kgb/errors.py), [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)

## See Also

- [FunctionSpy and Call Inspection](function-spy.md) — Deep dive into `FunctionSpy` and `SpyCall` records.
- [Advanced Operations: SpyOp* Classes](spy-ops.md) — Scripting multi-call returns and raises.
- [Changelog (NEWS.rst)](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst) — Version history and compatibility notes.

---

<a id='page-2'></a>

## Core Architecture: SpyAgency, FunctionSpy, and SpyCall

### Related Pages

Related topics: [Overview and Quick Start](#page-1), [Spy Operations and Assertions](#page-3)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)
- [kgb/calls.py](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)
- [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py)
- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)
- [kgb/ops.py](https://github.com/beanbaginc/kgb/blob/main/kgb/ops.py)
- [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)
- [kgb/errors.py](https://github.com/beanbaginc/kgb/blob/main/kgb/errors.py)
- [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)
</details>

# Core Architecture: SpyAgency, FunctionSpy, and SpyCall

## Overview

`kgb` is a function-spying library for Python unit tests. At its heart, three classes cooperate to intercept calls to a target function, record those calls, and assert on them later. `SpyAgency` is the lifecycle manager, `FunctionSpy` is the runtime interceptor that replaces the target's code, and `SpyCall` is the per-invocation recorder. Around them, a signature-introspection layer adapts to differences between CPython versions, and two entry points (`spy_on` context manager and the `spy_agency` pytest fixture) make the system ergonomic in both `unittest` and `pytest` suites.

```mermaid
flowchart LR
    User[Test Code] -->|spy_on / @spy_for| Agency[SpyAgency]
    Agency -->|creates & tracks| Spy[FunctionSpy]
    Spy -->|replaces __code__| Target[(Target Function)]
    Target -->|on call| Spy
    Spy -->|appends| Call[SpyCall]
    Spy -->|may invoke| Original[Original callable]
    Spy -->|may invoke| Op[SpyOp* / call_fake]
    Call -->|stored in| Spy
    Agency -->|assert_*| Call
```

## SpyAgency — The Coordinator

`SpyAgency` lives in [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py). It owns a `set` of every `FunctionSpy` it has ever created and exposes two kinds of behavior: lifecycle (`spy_on`, `spy_for`, `unspy`, `unspy_all`) and assertions (`assertSpyCalled`, `assertSpyCalledWith`, `assertSpyLastReturned`, etc.). Source: [kgb/agency.py:1-60]().

The class is designed to be either instantiated directly or **mixed in** to a `unittest.TestCase`. The constructor calls `super().__init__(*args, **kwargs)`, and a `tearDown()` method calls `self.unspy_all()` automatically — this is the mechanism that prevents the "spies leaking between tests" pitfall that users hit when running data-driven tests such as DDT. Source: [kgb/agency.py:18-50](). As reported in [issue #5](https://github.com/beanbaginc/kgb/issues/5), failing to unspy causes `ExistingSpyError` on subsequent runs; the agency pattern solves this by tying cleanup to the test case lifecycle.

From kgb 7.0 onward, `SpyAgency` also exposes `assert_spy_called`–style snake_case aliases and a `spy_agency` pytest fixture is provided in [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py), which yields a fresh `SpyAgency` and guarantees `unspy_all()` on teardown. Source: [kgb/pytest_plugin.py:1-30](); [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst).

## FunctionSpy — The Runtime Interceptor

`FunctionSpy` is defined in [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py). A single instance represents one spied function: it holds the original callable, the current fake (`call_fake`) or planned operation (`op`), the `calls` list of `SpyCall` records, and metadata about the function's owner and type. Source: [kgb/spies.py]().

The most invasive part of kgb is the `_build_spy_code` path: kgb synthesizes a new Python code object that impersonates the original function and forwards every call back into `_kgb_forwarding_call`, which delegates to the spy's `handle_call()`. To support closures, the synthesized function explicitly references each `co_freevars` name; to support the new exception state machinery in Python 3.11+, the code uses `code.replace(...)` rather than building a new `CodeType`. Source: [kgb/spies.py:build_spy_code](). This is precisely the area that had to be repaired for Python 3.13 compatibility in kgb 7.2 — see [issue #11](https://github.com/beanbaginc/kgb/issues/11), where setting `co_freevars` / `co_cellvars` on a replaced code object caused interpreter crashes. Source: [NEWS.rst:kgb 7.2]().

`FunctionSpy` is the only class that knows how to record a call. When the spied function is invoked, it constructs a `SpyCall`, normalizes the arguments (stripping the bound `self` for methods), executes the configured plan — original callable, `call_fake`, or a `BaseSpyOperation` subclass — and stores the resulting return value or exception on the `SpyCall`. The operation abstraction lives in [kgb/ops.py](https://github.com/beanbaginc/kgb/blob/main/kgb/ops.py) and ships with `SpyOpReturn`, `SpyOpRaise`, `SpyOpMatchAny`, and ordered variants.

## SpyCall — The Per-Invocation Recorder

`SpyCall` is defined in [kgb/calls.py](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py) and is created once per invocation. It stores `args`, `kwargs`, `return_value`, and `exception`, and provides boolean predicates (`called_with`, `returned`, `raised`, `raised_with_message`) plus `last_*` helpers that delegate to the most recent call in the owning spy's list. Source: [kgb/calls.py:1-80]().

This design means that all call-related state is **immutable per call** and **append-only on the spy**. Resetting state is a single operation: `FunctionSpy.reset_calls()` empties the list. Source: [kgb/spies.py](). Returning multiple different values across calls — the "double agent" pattern from [issue #3](https://github.com/beanbaginc/kgb/issues/3) — is therefore supported by reading `calls[0].return_value`, `calls[1].return_value`, etc., rather than by overloading the spy itself.

## Supporting Layer: Signature Introspection and Entry Points

Because the `__code__` replacement must match the target function's argument shape exactly, kgb wraps every intercepted function in a `FunctionSig` introspector. The base class `BaseFunctionSig` and the Python 3 implementation `FunctionSigPy3` are defined in [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py). They expose attributes such as `defined_func`, `has_getter`, `has_setter`, `is_slippery`, and `all_arg_names`, and provide `format_arg_spec()` for code generation. Source: [kgb/signature.py:1-120](). Version-specific subclasses exist so that positional-only arguments, `__wrapped__` unwrapping, and the `__code__.replace` API all work consistently.

Two user-facing entry points wire this machinery together:

- The `spy_on` **context manager** in [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py) creates a throwaway `SpyAgency`, yields the new `FunctionSpy`, and calls `unspy()` on exit. This is the right tool for ad-hoc, single-test spies.
- The `spy_agency` **pytest fixture** in [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py) is the idiomatic option for pytest users and was introduced in kgb 7.0.

The package's public surface is centralized in [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py), which re-exports `SpyAgency`, `spy_on`, and the `SpyOp*` operation classes, and defines `VERSION = (7, 3, 1, 'alpha', 0, False)`. Source: [kgb/__init__.py:1-30]().

## Common Failure Modes

A few issues recur in the community and map directly onto this architecture:

| Symptom | Root cause in architecture | Source |
|---|---|---|
| `ExistingSpyError` on second test (DDT) | `SpyAgency.unspy_all()` not run between iterations | [issue #5](https://github.com/beanbaginc/kgb/issues/5) |
| Crash on Python 3.13 b2 with closures | `_build_spy_code` mutated `co_freevars` / `co_cellvars` incorrectly | [issue #11](https://github.com/beanbaginc/kgb/issues/11) |
| Pytest fails on Python 3.10/3.11 | `assertRaisesRegexp` alias removal and bytecode changes | [issues #8, #9, #12](https://github.com/beanbaginc/kgb/issues/9) |
| `test_spy_on_generator` fails on PyPy3.11 | `__code__` mutation interacts with PyPy's JIT/object model | [issue #13](https://github.com/beanbaginc/kgb/issues/13) |

In every case, the underlying fix lives in either `FunctionSig` (introspection), `_build_spy_code` (interception), or the lifecycle methods on `SpyAgency`.

## See Also

- [Getting Started: Installing and Importing kgb]()
- [Spies in Depth: `call_fake`, `call_original`, and `SpyOp*`]()
- [pytest Integration: The `spy_agency` Fixture]()
- [Python Version Compatibility Matrix]()

---

<a id='page-3'></a>

## Spy Operations and Assertions

### Related Pages

Related topics: [Core Architecture: SpyAgency, FunctionSpy, and SpyCall](#page-2), [Testing Framework Integration and Platform Compatibility](#page-4)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [kgb/ops.py](https://github.com/beanbaginc/kgb/blob/main/kgb/ops.py)
- [kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py)
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)
- [kgb/calls.py](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)
- [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py)
- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)
- [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)
</details>

# Spy Operations and Assertions

`kgb` provides two complementary mechanisms for shaping what a spy *does* when it intercepts a call and what you *check* afterward. **Spy operations** (registered via `op=`) describe a planned sequence of behaviors — return values, raised exceptions, or matching rules — for each invocation of the spied function. **Assertions** are the read-side companion, letting a test verify that a spy (or a single `SpyCall`) was called, what it received, what it returned, and what it raised. Together they let a test author replace real side effects and then verify the interactions deterministically.

## 1. Spy Operations (`op=`)

A spy operation is an object that decides how to handle each intercepted call. They are registered when a spy is set up, by passing an `op=` argument to `spy_on()` or `@spy_for()`. The base class lives in [kgb/ops.py](https://github.com/beanbaginc/kgb/blob/main/kgb/ops.py) and exposes a `handle_call(spy_call, *args, **kwargs)` method that returns a value or raises an exception.

The built-in operations, re-exported from [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py), are:

| Class | Behavior |
| --- | --- |
| `SpyOpReturn(value)` | Always return a fixed value. |
| `SpyOpRaise(exception)` | Always raise a fixed exception (or exception instance). |
| `SpyOpReturnInOrder([...])` | Return successive values across calls. |
| `SpyOpRaiseInOrder([...])` | Raise successive exceptions across calls. |
| `SpyOpMatchInOrder([...])` | Match expected calls (args/kwargs) in order; raise `UnexpectedCallError` on mismatch. |
| `SpyOpMatchAny([...])` | Match expected calls in any order; raise `UnexpectedCallError` on mismatch. |

Each `Match*` entry is a dict that may itself include an inner `op` key, allowing nested reusable rule sets. Example from the release notes:

```python
spy_on(lockbox.enter_code, op=kgb.SpyOpMatchInOrder([
    {
        'args': (42, 42, 42, 42, 42, 42),
        'op': kgb.SpyOpRaise(Kaboom()),
        'call_original': True,
    },
]))
```

`BaseSpyOperation.setup()` returns a fake function the spy will install as a forwarding trampoline, so the spy framework can treat any operation uniformly ([kgb/ops.py](https://github.com/beanbaginc/kgb/blob/main/kgb/ops.py)). When a call fails to match, `UnexpectedCallError` is raised — and since kgb 7, the error message includes the call that was actually made, simplifying debugging.

## 2. Recording Calls: `FunctionSpy` and `SpyCall`

Every intercepted invocation is recorded by the spy as a `SpyCall`, accessible from `function.calls` ([kgb/calls.py](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)). A `SpyCall` stores `args`, `kwargs`, `return_value`, and `exception`, and exposes predicates such as `called_with(...)`, `returned(...)`, `raised(...)`, and `raised_with_message(...)`. These predicates are the building blocks used by all higher-level assertions.

The spy itself is the actual function object once spying is active — meaning code that calls `obj.function` is transparently rerouted through kgb's forwarding call ([kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)). For type-checker-friendly access, kgb 7.3 added `SpyAgency.get_spy()`, which returns the `FunctionSpy` for a given callable without relying on the replaced function object.

## 3. Assertion Surface: `SpyAgency`, `kgb.asserts`, and the pytest fixture

The primary assertion API lives on `SpyAgency` in [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py). Both `unittest`-style (camelCase, e.g., `assertSpyCalledWith`) and snake_case (`assert_spy_called_with`) forms are provided. When `SpyAgency` is mixed into a `unittest.TestCase`, its `tearDown()` automatically unspies everything ([kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)).

For non-unittest frameworks, every `assert_*` method is also re-exported as a standalone function in [kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py), backed by a module-level `SpyAgency` instance. The module loops over `vars(SpyAgency)` and dynamically attaches every method whose name starts with `assert_` to its `__all__`.

For pytest users, [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py) provides a `spy_agency` fixture that yields a fresh `SpyAgency` and calls `unspy_all()` on teardown. The same `assert_*` methods are available on this fixture object.

A representative subset of the available assertions:

| Assertion | Checks |
| --- | --- |
| `assert_has_spy(spy)` / `assert_spy_called(spy)` | The function was ever called. |
| `assert_spy_not_called(spy)` | The function was never called. |
| `assert_spy_call_count(spy, n)` | The function was called exactly `n` times. |
| `assert_spy_called_with(spy, *a, **kw)` | Any call matched the given arguments. |
| `assert_spy_last_called_with(spy, *a, **kw)` | The most recent call matched. |
| `assert_spy_called_once_with(spy, *a, **kw)` | Exactly one call matched. |
| `assert_spy_raised(spy, exc)` / `assert_spy_last_raised(...)` | A call (or the last one) raised an exception class. |
| `assert_spy_raised_message(spy, exc, msg)` / `assert_spy_last_raised_message(...)` | A call (or the last) raised with a matching message. |
| `assert_spy_returned(spy, value)` / `assert_spy_last_returned(...)` | A call (or the last) returned a given value. |

Most assertions accept either a callable or a `FunctionSpy`; some (`called_with`, `returned`, `raised`) also accept an individual `SpyCall`, which is useful when asserting on a specific recorded call rather than the whole history ([kgb/calls.py](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)).

## 4. Lifecycle and Common Failure Modes

Spies are typically created via one of four patterns described in the README: instantiating `SpyAgency`, mixing it into a test class, using `@spy_for` as a decorator (kgb 6+), or the standalone `spy_on` context manager in [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py). The context-manager form creates a throwaway `SpyAgency`, spies for the duration of the `with` block, and then calls `unspy()`.

A few failure modes worth noting:

- **Double-spying.** Attempting to spy on a function that is already spied raises `ExistingSpyError`. This commonly surfaces when using parameterized test runners (e.g., DDT) that re-enter the same spy across iterations; the fix is to ensure the agency is torn down or `unspy()` is called between runs.
- **Decorator-wrapped functions.** Some decorators do not preserve `__name__`, which can confuse kgb's signature handling. kgb 7 introduced a `func_name=` argument to `spy_on()` so callers can declare the original function name explicitly.
- **Mismatch errors.** When using `SpyOpMatchInOrder` / `SpyOpMatchAny`, any unexpected call raises `UnexpectedCallError`; since kgb 7 the message lists the actual call that triggered the failure.
- **Generator functions.** Spying on generator functions is supported, but tests must be aware that the spy wraps the generator object. A known issue exists on PyPy 3.11 where one specific generator-related test fails; the upstream fix tracks changes in how PyPy builds and executes generator bytecodes.

For deeper coverage of the bytecode-injection mechanics that make `spy_on()` transparent to callers, see the `FunctionSig` and `_build_spy_code` helpers in [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py).

## See Also

- [kgb README on PyPI](https://pypi.org/project/kgb/) — installation, quick start, and high-level examples.
- kgb 7.2 release notes — Python 3.13 compatibility fixes for function composition and execution.
- kgb 7.3 release notes — Python 3.14 support, `SpyAgency.get_spy()`, generator `DeprecationWarning` fix.
- Review Board project — kgb's primary production user, providing real-world examples of spy operations and assertions.

---

<a id='page-4'></a>

## Testing Framework Integration and Platform Compatibility

### Related Pages

Related topics: [Overview and Quick Start](#page-1), [Spy Operations and Assertions](#page-3)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)
- [conftest.py](https://github.com/beanbaginc/kgb/blob/main/conftest.py)
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py)
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)
- [kgb/utils.py](https://github.com/beanbaginc/kgb/blob/main/kgb/utils.py)
- [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)
- [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)
</details>

# Testing Framework Integration and Platform Compatibility

kgb integrates with the major Python testing ecosystems (pytest, unittest, and standalone scripts) and supports a broad matrix of Python interpreters (CPython 2.7, 3.6 through 3.14, and PyPy). Because kgb's core mechanism is replacing the `__code__` attribute of a function, every interpreter release can shift the surface it relies on, so platform compatibility and framework integration are first-class concerns of the project.

## Integration with pytest

kgb ships as a native pytest plugin that exposes a `spy_agency` fixture. When the plugin is loaded, any pytest test function can request the fixture and receive a ready-to-use `SpyAgency` instance. The fixture guarantees that every spy registered during the test is torn down automatically.

Source: [kgb/pytest_plugin.py:9-22](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)

```python
@pytest.fixture
def spy_agency():
    agency = SpyAgency()
    try:
        yield agency
    finally:
        agency.unspy_all()
```

The fixture pattern means tests do not need to instantiate `SpyAgency` manually and do not need to remember to call `unspy_all()` in cleanup. The repository's `conftest.py` configures pytest by ignoring Python-3-only test files when running under Python 2, so the same suite can be exercised on both interpreter families without manual exclusion.

Source: [conftest.py:5-10](https://github.com/beanbaginc/kgb/blob/main/conftest.py)

## Integration with unittest and Standalone Use

For projects using `unittest`, `SpyAgency` is designed as a mixin that combines with any `TestCase` subclass. Its `__init__` forwards positional and keyword arguments through `super()`, allowing it to compose cleanly with other mixins such as `django.test.TestCase`.

Source: [kgb/agency.py:33-49](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)

When mixed into a `TestCase`, the agency's `tearDown()` calls `super().tearDown()` and then `self.unspy_all()`, ensuring every spy registered during the test is removed before the next test runs. This prevents cross-test leakage and avoids the `ExistingSpyError` that arises when the same function is spied twice in the same run (see Issue #5).

For lightweight, one-shot spying without managing an agency, kgb provides the `spy_on` context manager. It instantiates a private `SpyAgency`, registers a spy, yields it to the caller, and calls `unspy()` from the `finally` block.

Source: [kgb/contextmanagers.py:11-31](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py)

```python
from kgb import spy_on

with spy_on(bomb.explode, call_original=False):
    bomb.explode()
```

## Python Version Compatibility

kgb must function across many Python versions because bytecode replacement interacts with interpreter internals that change between releases. Compatibility is handled through version-specific introspection classes and a top-level factory exported as `FunctionSig`.

Source: [kgb/signature.py:122-130](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)

```python
if sys.version_info[0] == 2:
    FunctionSig = FunctionSigPy2
elif sys.version_info[0] == 3:
    FunctionSig = FunctionSigPy3
else:
    raise Exception('Unsupported Python version')
```

`FunctionSigPy2` uses the legacy `inspect.formatargspec()` API to reconstruct argument lists, while `FunctionSigPy3` walks `inspect.Signature.parameters` and preserves positional-only, keyword-only, `*args`, and `**kwargs` parameter kinds. Common helpers like `get_defined_attr_value` and `is_attr_defined_on_ancestor` live in `kgb/utils.py` so that signature and spy logic can share a stable descriptor-walking implementation across interpreter versions.

Source: [kgb/utils.py:12-39](https://github.com/beanbaginc/kgb/blob/main/kgb/utils.py)

The release history records the supported Python matrix. kgb 7.0 dropped Python 2.6, 3.4, and 3.5. kgb 7.1 added Python 3.11, kgb 7.2 added Python 3.13 (with fixes for closure-related crashes), and kgb 7.3 added Python 3.14 while also shipping the LICENSE file for downstream packaging.

Source: [NEWS.rst:1-30](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)

## Platform Support Matrix

The table below summarizes the integration modes kgb advertises and the lifecycle each one provides.

| Integration Mode | Mechanism | Cleanup Hook | Available Since |
| --- | --- | --- | --- |
| pytest fixture | `spy_agency` fixture | `finally: agency.unspy_all()` | kgb 7.0 |
| unittest mixin | `class Foo(SpyAgency, TestCase)` | `SpyAgency.tearDown()` | kgb 0.5 |
| Context manager | `with spy_on(...)` | `finally: spy.unspy()` | kgb 0.5 |
| Standalone asserts | `from kgb.asserts import ...` | n/a (read-only helpers) | kgb 7.0 |

For interpreter coverage, kgb 7.3 explicitly supports CPython 2.7 and 3.6 through 3.14, plus PyPy. The internal `sys.version_info` switch in `signature.py` still gates Py2 vs Py3 paths, while Python 3 minor-version differences (such as positional-only parameters introduced in 3.8) are absorbed inside `FunctionSigPy3`.

Source: [kgb/signature.py:122-130](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)

## Common Compatibility Pitfalls

Three patterns recur in community reports:

1. **PyPy interpreter quirks** — bytecode replacement behaves differently on PyPy, and `test_spy_on_generator` remains a known failure on PyPy 3.11 (Issue #13). CPython support is broad, but PyPy compatibility is best-effort.
2. **Removed unittest aliases** — `assertRaisesRegexp` was deprecated in Python 3.2 and removed in 3.12 (Issue #12). Test code using the deprecated alias must migrate to `assertRaisesRegex`.
3. **Closure capturing under Python 3.13 beta** — spies that captured closures crashed on Python 3.13 beta builds (Issue #11). This was fixed in kgb 7.2 by adapting to the new function composition model.

Source: [GitHub Issues #11, #12, #13](https://github.com/beanbaginc/kgb/issues)

The README also documents that kgb can be mixed into any `TestCase` subclass and combined with frameworks such as `asynctest` for asynchronous tests (Issue #4), making it a drop-in choice across most Python testing styles.

Source: [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)

## See Also

- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py) — `SpyAgency` mixin and assertion methods
- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py) — pytest `spy_agency` fixture
- [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py) — `spy_on` standalone context manager
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py) — interpreter-version dispatch for `FunctionSig`
- [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst) — version-by-version compatibility history

---

<!-- evidence_pipeline_checked: true -->
<!-- evidence_injected: true -->

---

## Pitfall Log

Project: beanbaginc/kgb

Summary: Found 12 structured pitfall item(s), including 2 high/blocking item(s). Top priority: Installation risk - Installation risk requires verification.

## 1. Installation risk - Installation risk requires verification

- Severity: high
- Evidence strength: source_linked
- Finding: Project evidence flags a installation risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: community_evidence:github | https://github.com/beanbaginc/kgb/issues/13

## 2. Configuration risk - Configuration risk requires verification

- Severity: high
- Evidence strength: source_linked
- Finding: Project evidence flags a configuration risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: community_evidence:github | https://github.com/beanbaginc/kgb/issues/11

## 3. Installation risk - Installation risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a installation risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: community_evidence:github | https://github.com/beanbaginc/kgb/issues/8

## 4. Installation risk - Installation risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a installation risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: community_evidence:github | https://github.com/beanbaginc/kgb/issues/9

## 5. Capability evidence risk - Capability evidence risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: README/documentation is current enough for a first validation pass.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: capability.assumptions | github_repo:10215090 | https://github.com/beanbaginc/kgb

## 6. Runtime risk - Runtime risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a runtime risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: community_evidence:github | https://github.com/beanbaginc/kgb/issues/5

## 7. Maintenance risk - Maintenance risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a maintenance risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: community_evidence:github | https://github.com/beanbaginc/kgb/issues/12

## 8. Maintenance risk - Maintenance risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a maintenance risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: evidence.maintainer_signals | github_repo:10215090 | https://github.com/beanbaginc/kgb

## 9. Security or permission risk - Security or permission risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: no_demo
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: downstream_validation.risk_items | github_repo:10215090 | https://github.com/beanbaginc/kgb

## 10. Security or permission risk - Security or permission risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: no_demo
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: risks.scoring_risks | github_repo:10215090 | https://github.com/beanbaginc/kgb

## 11. Maintenance risk - Maintenance risk requires verification

- Severity: low
- Evidence strength: source_linked
- Finding: issue_or_pr_quality=unknown。
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: evidence.maintainer_signals | github_repo:10215090 | https://github.com/beanbaginc/kgb

## 12. Maintenance risk - Maintenance risk requires verification

- Severity: low
- Evidence strength: source_linked
- Finding: release_recency=unknown。
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: evidence.maintainer_signals | github_repo:10215090 | https://github.com/beanbaginc/kgb

<!-- canonical_name: beanbaginc/kgb; human_manual_source: deepwiki_human_wiki -->
