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.
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.
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
- You are writing an operator or controller. There are informers and leader election here, but no workqueue, no rate-limited requeue, and no finalizer or status handling, so you would rebuild half of kopf badly
- Ergonomics matter to your team. Generated methods take (self, namespace, **kwargs), so your editor shows you nothing about valid arguments and you go read the generated markdown for every call. lightkube and pykube-ng were written specifically because this is unpleasant
- You are watching a container image budget. The package unpacks to about 83 MB and pulls requests, aiohttp, urllib3, websocket-client, oauthlib, pyyaml, python-dateutil, durationpy, certifi, and six
- Your fleet spans several Kubernetes minor versions. The client version has to track the cluster: 36.y.z targets 1.36, and anything more than one minor away is marked as partial in the project's own compatibility matrix
- You need reliable long-running watches without extra work. The Watch docstring says a 410 Gone means you have to recover yourself, and passing timeout_seconds quietly turns off the built-in retry loop, so naive watch loops die overnight
- You want a fully async client. kubernetes.aio ships generated client and config modules, but there is no kubernetes.aio.watch and no async informer, so event streaming falls back to threads
- Your work is mostly custom resources. Those have no generated models, so you are back to raw dicts through CustomObjectsApi or the dynamic client and get none of the typed attribute access that justifies the package size
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:
breakThe 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:
raiseEvery 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
| Package | Registry | Pick it when |
|---|---|---|
| kopf | PyPI | You are building an operator and want handlers, retries, finalizers, and status updates instead of assembling a control loop yourself |
| lightkube | PyPI | You want a small typed client with a consistent get/list/apply interface and far fewer dependencies than the generated one |
| pykube-ng | PyPI | You want a light object-oriented wrapper for scripts and are happy working close to raw API objects |