# Kopf makes an operator a decorated function, and its author calls the framework finished

> Kopf is a Python framework that turns Kubernetes operator work into decorated functions, with retries, peering and per object state handled underneath. It is stable at semantic v1, and the maintainer states plainly that no new major functionality is planned.

**nolar/kopf** — A Python framework to write Kubernetes operators in just a few lines of code

- Repository: https://github.com/nolar/kopf
- Website: https://docs.kopf.dev/
- Stars: 2,648 · Forks: 200
- Language: Python
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/nolar-kopf

## Two files, and the watch loop is a decorator

The smallest operator in the examples directory is one decorated function and an import. The decorator names the resource kind it cares about, and the function receives that object's fields unpacked as keyword arguments:

```python
import kopf

@kopf.on.create('kopfexamples')
def create_fn(spec, name, meta, status, **kwargs):
    print(f"And here we are! Created {name} with spec: {spec}")
```

That unpacking is the framework's central move and it runs in both directions. Resource data becomes handler arguments, whatever a handler returns is marshalled into the object's status, and log messages become Kubernetes events attached to the resource, which is why an operator written this way carries no API communication code of its own. The documented argument list is body, meta, spec, status, name, namespace, retry, diff, old, new and logger. Handlers can be plain functions or coroutines, and a plain one is run in a thread under the hood, so a blocking call inside it does not stall the others.

## The version comes from git tags and every dependency is annotated in megabytes

The build section requires setuptools with setuptools-scm 8.1 or newer, and the project table marks the version as dynamic, with a comment saying it is taken from git tags, so there is no version string to edit in the tree. Six direct requirements carry size comments beside them: python-json-logger at 0.05 MB, iso8601 at 0.07 MB, pyyaml at 0.90 MB, click at 0.60 MB with a floor of 8.2.0, aiohttp at 7.80 MB with a floor of 3.14.0, and jsonpatch with no annotation at all. The weight sits in the optional groups. The full-auth extra pulls pykube-ng at 4.90 MB and the official client at 12.0.0 or newer, whose 40.0 MB comment ends in an exclamation mark. The uvloop extra adds 9.00 MB, with a second entry pinned to 0.18.0 for Python 3.12 and above. The command line surface is one script pointing at kopf.cli:main.

## Packaging is three Dockerfile lines and the dev loop mounts a reduced kubeconfig

The deployment assumption is stated without ceremony: when the operator runs inside the cluster it is packaged into a Docker image by whatever CI/CD tool you already use. The example image is three instructions long.

```dockerfile
FROM python:3.14
ADD . /src
RUN pip install kopf
CMD kopf run /src/handlers.py --verbose
```

The script that kopf run points at is your handlers file, and the examples tree keeps one example.py per topic. For trying an operator without deploying it, a prebuilt image carrying every extra is published on GHCR and the only thing you mount is your own file. The first step of that loop deliberately shrinks the credential you hand over:

```bash
# Minimize the credentials exposure.
kubectl config view --minify --flatten > dev.kubeconfig
```

The container then runs with host networking, the handler file mounted read only at /app/main.py, and that reduced kubeconfig bound into the root credentials path.

## Peering keeps a second copy of your operator off the same object

Running one operator on your laptop while the deployed copy is still in the cluster is the situation this framework has a name for. Operators can be given distinct identities, so two copies serving the same resource kinds become aware of each other across pods and an object is processed once rather than twice. While a dev mode operator runs outside the cluster, the deployed one is paused. A peering.yaml sits in the repository root beside the import linter configuration, and peering is the sixth of the numbered example directories. The same mode covers admission webhook handlers, which can also be tunnelled to a dev mode operator, and the filtering system can silence a handler completely, matching on arbitrary functions, on label and annotation values, on presence or absence, or on a callback that decides per object.

## Daemons and timers cover work that outlives a single event

A creation handler runs once, so anything that must keep working while an object exists gets its own decorator. A daemon is a function that loops until the framework sets a flag:

```python
import time
import kopf

@kopf.daemon('kopfexamples')
def my_daemon(spec, stopped, **kwargs):
    while not stopped:
        print(f"Object's spec: {spec}")
        time.sleep(1)
```

The stopped argument is the loop condition the framework controls, and a daemon can be a thread or an asyncio task. Timers are the same idea with an interval instead of a loop:

```python
import kopf

@kopf.timer('kopfexamples', interval=1)
def my_timer(spec, **kwargs):
    print(f"Object's spec: {spec}")
```

A timer can also carry a delay measured from the last change, so a settling period lives in the decorator rather than in a sleep. Handlers can be narrowed to selected fields instead of whole objects, and sub-handlers can be generated dynamically or made conditional for each object.

## Progress survives a restart, and retries come with exceptions of their own

Eventual consistency is handled by retrying a handler after any error until it succeeds, with special exceptions for the cases where that default is wrong: raise one to request another attempt, or to declare that no further attempt should ever be made. Limits can be set on the number of attempts or on the elapsed time, and the progress of a running handler is persisted implicitly, so an operator restarted mid reconcile carries on rather than starting over. The same tolerance covers an object that changed during a long downtime, because those changes are picked up afterwards. Values that must not be serialized into the API server can be kept in per resource in memory containers instead, and live in memory indexing is available for resources or for excerpts of them.

## The author calls the framework finished and ships maintenance only

The status note is blunt about scope: no active development of new major functionality, because the idea of a framework for operators is fully expressed and implemented, and the piece is finished. Minor feature requests are still implemented from time to time, major bugs are fixed as soon as possible, and there were none for a long time. What is promised is maintenance for new Python and Kubernetes versions, plus internal work on memory footprint, high load readiness and agentic friendliness, all of it backwards compatible, with no semantic v2 on the horizon. The releases match that: 1.44.4, 1.44.5 and 1.44.6 all landed inside one minor line between March and June 2026, and the last push to the default branch is dated 22 September 2026. The repository carries 2640 stars, 200 forks and 183 open issues.

## Conclusion

Reach for Kopf when you want an operator written as ordinary Python functions with decorators instead of a controller scaffold, and when you also want retries, peering and per object state handled for you. Check three things before committing. Confirm your interpreter is 3.10 or newer, since that is the declared floor and the classifiers stop at 3.14. Read up on peering before running a dev mode operator against a cluster that already has one deployed, because the pause behaviour is what keeps the two apart. And decide early about the full auth extra, since the official Kubernetes client it pulls in is annotated at 40.0 MB in the dependency file. Not a fit if you need new major features on a schedule, because the maintainer has said the framework is done.

## FAQ

### What Python version does Kopf require?

3.10 or newer. The dependency table sets requires-python to >=3.10 and the classifiers list 3.10 through 3.14. CPython and PyPy are the implementations that are officially supported and tested, and other implementations can work too.

### How do I run a Kopf operator locally before deploying it?

Run the prebuilt image published on GHCR with all extras, mount your handler file into it, and give it a kubeconfig reduced by kubectl config view --minify --flatten so fewer credentials are exposed. Host networking is used so a local cluster is reachable.

### What happens when two Kopf operators watch the same resource kind?

Give each operator its own identity and the peering mechanism makes copies aware of each other across pods, so an object is not processed twice. A deployed operator is also paused while a dev mode operator runs outside the cluster.

### Is Kopf still getting new features?

Major bugs are fixed as soon as possible and maintenance for new Python and Kubernetes versions is done regularly, but there is no active development of new major functionality. Minor feature requests are implemented from time to time, and no semantic v2 with breaking changes is planned.

## Sources

- [License: MIT](https://github.com/nolar/kopf/blob/main/LICENSE)
- [nolar/kopf on GitHub](https://github.com/nolar/kopf)
- [Project website](https://docs.kopf.dev/)
- [README](https://github.com/nolar/kopf/blob/main/README.md)
- [Releases](https://github.com/nolar/kopf/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/nolar-kopf
