mrkeyoor.com_
Thu 06 Aug 07:42 UTC
PyPIInfraupdated 06 Aug 2026

kubernetes

This is the official Python client for the Kubernetes API, maintained inside the Kubernetes project itself. Almost all of it is generated from the cluster's OpenAPI specification, which is why the package ships 65 Api classes and over 700 V1 model classes: one method per API endpoint and one class per resource kind. You pick the right Api class for the group you want (CoreV1Api for pods and services, AppsV1Api for deployments, BatchV1Api for jobs), authenticate with config.load_kube_config() locally or config.load_incluster_config() inside a pod, and call methods like list_namespaced_pod or patch_namespaced_deployment. Around the generated core are hand-written helpers that matter: watch for event streams, stream for exec and port-forward over websockets, dynamic for resources with no generated model, informer for a cached shared watch, leaderelection, and utils.create_from_yaml for applying manifests.

Verdict

For scripts and in-cluster jobs that need complete, always-current API coverage, this is the client to use and the only one guaranteed to match the server spec. If you are building a controller, or if the generated call style is slowing your team down, reach for kopf or lightkube instead of fighting 700 model classes.

API stability4/5The method naming scheme and the config and watch helpers have been consistent for years, but because the code is regenerated per Kubernetes release, model attributes appear and disappear with upstream API changes and the project publishes a version-to-cluster compatibility matrix precisely because skew is expected.
Docs3/5Coverage is complete but it is generated markdown, one page per Api class, with no narrative guide; real usage has to be learned from the examples directory, and the generated (self, namespace, **kwargs) signatures mean the docs are the only place valid parameters are listed.
Maintenance5/5Maintained by the Kubernetes project with a release per upstream minor, repo pushed 6 August 2026, 36.0.3 published 13 July 2026, and 73 open issues (85 counting PRs) for a client this size.
Ecosystem5/5Roughly 26.7M weekly downloads and it is the base layer under Airflow's Kubernetes executor, Kubeflow, Ray, and most Python operators and cluster tooling.

Use it if

  • You are automating cluster operations from Python: scaling deployments, rotating secrets, draining nodes, or cleaning up finished jobs on a schedule
  • Your code runs inside the cluster and needs the API. load_incluster_config() reads the mounted service account token and CA with no extra wiring
  • You need to exec into a container or port-forward from Python, which the stream helpers do over websockets and most third-party clients do not implement
  • You are building inventory or dashboard tooling that lists across namespaces with label and field selectors and needs the typed model attributes
  • You want to be on the client that tracks the API surface exactly: it is generated from the same OpenAPI spec the server publishes, so new fields appear the release they land
Skip it if

Setup reality

pip install kubernetes needs Python 3.10 or newer and lands about 83 MB in site-packages with ten runtime dependencies, six among them in 2026. There is no native code, so installs are fast everywhere. The first real decision is the version: pick the client major that matches your cluster's minor from the compatibility table in the README, and pin it, because upgrading the client can change model attributes when the upstream API changes. Authentication is where local setups break. load_kube_config() honours your kubeconfig including exec credential plugins, which means aws eks get-token, gke-gcloud-auth-plugin, or az must be installed and on PATH or you get a ConfigException that says nothing about the missing binary. GCP application default credentials need the google-auth extra: pip install 'kubernetes[google-auth]'. Inside a pod use load_incluster_config() instead, and give the service account an RBAC role, since a 403 surfaces as a generic ApiException you have to read the body of.

Patterns

Authenticate from a kubeconfig or from inside a podload-config

from kubernetes import client, config
from kubernetes.config.config_exception import ConfigException

try:
    config.load_incluster_config()      # running as a pod
except ConfigException:
    config.load_kube_config()           # local kubeconfig

v1 = client.CoreV1Api()
print(v1.list_namespace().items[0].metadata.name)

This try/except is the standard shape because the same script usually runs both places. load_kube_config accepts context= to pick a cluster; without it you silently get whatever your current-context happens to be, which is how test scripts hit production.

List resources filtered by labels and fieldslist-with-selectors

v1 = client.CoreV1Api()

pods = v1.list_namespaced_pod(
    namespace='default',
    label_selector='app=web,tier!=canary',
    field_selector='status.phase=Running',
    _request_timeout=30,
)
for p in pods.items:
    print(p.metadata.name, p.status.pod_ip)

Generated methods are typed as (self, namespace, **kwargs), so nothing autocompletes and a misspelled keyword raises TypeError at call time. Field selectors are only supported on a small set of fields per resource; the server rejects anything else with a 400.

Page through a big result setpaginate-large-lists

v1 = client.CoreV1Api()
continue_token = None

while True:
    resp = v1.list_pod_for_all_namespaces(limit=500, _continue=continue_token)
    for pod in resp.items:
        handle(pod)
    continue_token = resp.metadata._continue
    if not continue_token:
        break

The keyword is _continue with a leading underscore because continue is a Python keyword, and the token comes back on resp.metadata._continue. Without limit the API server materializes every object at once, which is how a listing script OOMs a large cluster's apiserver.

Create resources from a YAML fileapply-manifest

from kubernetes import client, config, utils

config.load_kube_config()
k8s = client.ApiClient()

utils.create_from_yaml(k8s, 'deployment.yaml', namespace='staging')

# or apply semantics instead of create
utils.create_from_yaml(k8s, 'deployment.yaml', apply=True)

The default behaviour is create, so a second run raises FailToCreateError with a 409 for each existing object; catch it or pass apply=True. Multi-document YAML works, but ordering is file order, so a Deployment ahead of its Namespace fails.

Change one field without a read-modify-writepatch-resource

apps = client.AppsV1Api()

apps.patch_namespaced_deployment(
    name='web',
    namespace='default',
    body={'spec': {'replicas': 5}},
)

# scale subresource is cheaper for just replicas
apps.patch_namespaced_deployment_scale(
    name='web', namespace='default', body={'spec': {'replicas': 5}}
)

Patch sends a strategic merge patch by default, so lists of containers merge on name rather than being replaced. To delete a field you set it to None in the body; omitting it leaves the existing value untouched.

Stream changes to a resourcewatch-events

from kubernetes import client, config, watch

config.load_kube_config()
v1 = client.CoreV1Api()
w = watch.Watch()

for event in w.stream(v1.list_namespaced_pod, namespace='default'):
    print(event['type'], event['object'].metadata.name)
    if done:
        w.stop()

Passing timeout_seconds disables the client's internal retry loop, so the generator just ends when the server closes the connection. The docstring is explicit that a 410 Gone after the resourceVersion expires is yours to recover from, usually by relisting and restarting the watch.

Run a command inside a containerexec-into-pod

from kubernetes.stream import stream

resp = stream(
    v1.connect_get_namespaced_pod_exec,
    'web-0',
    'default',
    command=['/bin/sh', '-c', 'cat /etc/hostname'],
    container='app',
    stderr=True,
    stdout=True,
    stdin=False,
    tty=False,
    _preload_content=True,
)
print(resp)

You must call it through kubernetes.stream.stream, not the Api method directly, or you get a raw HTTP response instead of a websocket. With _preload_content=False you get a channel object you can read incrementally, which is what you need for long-running commands.

Read and write CRDs with no generated modelcustom-resources

co = client.CustomObjectsApi()

obj = co.get_namespaced_custom_object(
    group='cert-manager.io',
    version='v1',
    namespace='default',
    plural='certificates',
    name='web-tls',
)
print(obj['status']['conditions'])

Everything here is plain dicts, so you lose attribute access and any type checking. plural is the resource name from the CRD (certificates), not the kind (Certificate), and getting that wrong returns a 404 that reads like the object is missing.

Work with any resource kind by discoverydynamic-client

from kubernetes import config, dynamic
from kubernetes.client import api_client

client_ = dynamic.DynamicClient(
    api_client.ApiClient(configuration=config.load_kube_config())
)

api = client_.resources.get(api_version='apps/v1', kind='Deployment')
for d in api.get(namespace='default').items:
    print(d.metadata.name, d.spec.replicas)

Discovery hits the API server on first use and caches to disk, so an unreachable cluster fails at resources.get rather than at the query. This is the way to write code that handles resources you do not know at build time, at the cost of an extra round trip.

Distinguish not-found from forbiddenhandle-api-errors

import json
from kubernetes.client.rest import ApiException

try:
    v1.read_namespaced_secret('db-creds', 'default')
except ApiException as exc:
    if exc.status == 404:
        create_secret()
    elif exc.status == 403:
        reason = json.loads(exc.body)['message']
        raise PermissionError(reason) from exc
    else:
        raise

Every failure is one ApiException, so branching on exc.status is the only option. The useful detail lives in exc.body as a JSON-encoded Status object; exc.reason is just the HTTP reason phrase and tells you nothing about which RBAC rule was missing.

Keep a local cache instead of pollingshared-informer

from kubernetes import client, config
from kubernetes.informer import SharedInformer, ADDED, MODIFIED, DELETED

config.load_kube_config()
v1 = client.CoreV1Api()

informer = SharedInformer(
    v1.list_pod_for_all_namespaces,
    resync_period=300,
    label_selector='app=web',
)
informer.add_event_handler(ADDED, lambda obj: print('added', obj.metadata.name))
informer.add_event_handler(DELETED, lambda obj: print('gone', obj.metadata.name))
informer.start()

pods = informer.cache.list()          # served from memory
one = informer.cache.get_by_key('default/web-0')

One handler per event type, and an unknown event_type raises ValueError rather than being ignored. Reading from informer.cache is the point: it avoids polling the API server. It runs on a background thread with no workqueue, so a slow handler blocks the event stream and handlers must be thread-safe.

Call the API from asyncioasync-client

import asyncio
from kubernetes.aio import client, config
from kubernetes.aio.client.api_client import ApiClient

async def main():
    await config.load_kube_config()
    async with ApiClient() as api:
        v1 = client.CoreV1Api(api)
        pods = await v1.list_pod_for_all_namespaces()
        for p in pods.items:
            print(p.metadata.namespace, p.metadata.name)

asyncio.run(main())

The async ApiClient must be used as a context manager or the aiohttp session leaks and you get 'Unclosed client session' on shutdown. There is no kubernetes.aio.watch, so streaming events from async code still means running the sync Watch in a thread executor.

Alternatives

PackageRegistryPick it when
kopfPyPIYou are building an operator and want handlers, retries, finalizers, and status updates instead of assembling a control loop yourself
lightkubePyPIYou want a small typed client with a consistent get/list/apply interface and far fewer dependencies than the generated one
pykube-ngPyPIYou want a light object-oriented wrapper for scripts and are happy working close to raw API objects