stripe-python: dynamic StripeObject classes, the StripeClient v1 namespace, and three Python version floors
Python library for the Stripe API.
At a glance
- What is it?
- stripe-python wraps the Stripe API in classes that populate themselves from each JSON response and routes calls through a StripeClient that namespaces resources under v1. Its install metadata, its classifiers and its own deprecation note give three different answers about which Python versions it runs on.
- Who is it for?
- Adopt stripe-python when a service talks to the Stripe API from Python and you want typed resources plus per-call API version pinning. Do not adopt it for a single webhook endpoint, where the shipped raw request example is less code.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Dynamic classes built from each API response
stripe-python exists because the Stripe API answers in JSON while application code wants named objects with predictable methods. The library ships a set of classes for API resources, and each one fills itself in from whatever the API sent back, which is how one installed copy keeps working across a wide range of versions of the Stripe API.
Every resource is a subclass of `StripeObject`, and that class stopped inheriting from `dict` in v15. Printing a resource still shows a dict-like representation, yet the object itself is not a dict, so a field name can never collide with a builtin method. `subscription.items` is the case picked out for this reason, because `items` is a dict method that payment code reaches for constantly.
Because the object is not a dict, reading it takes a few different spellings:
customer = client.v1.customers.retrieve("cus_123456789")
customer.email # attribute access
customer["email"] # subscript access
"email" in customer # membership
getattr(customer, "discount", None) # tolerate a field that may be absentThe last line is the habit worth keeping after a v15 upgrade, since a field the API did not send now comes back as `None` instead of raising.
StripeClient namespaces every resource under v1
`StripeClient` arrived in v8, and the module-level pattern it replaced is still available. The client holds the secret key, and resource calls hang off a v1 namespace, so `client.v1.customers.list()` and `client.v1.customers.retrieve("cus_123456789")` are the entry points. Per-call settings travel in an `options` dict, which is how one process can act as a connected account or pin an API date without constructing a second client:
client.v1.customers.retrieve(
"cus_123456789",
options={
"api_key": "sk_test_...",
"stripe_account": "acct_...",
"stripe_version": "2019-02-19",
}
)Read the configuration paragraph closely, because it does not match the code right after it. The prose tells you to set `stripe.api_key`, a global belonging to the legacy pattern, and the next block builds `StripeClient("sk_test_...")` without touching that global. Both styles work for existing endpoints today. The difference arrives later: once the legacy pattern is marked as deprecated, new endpoints will be reachable only through `StripeClient`, and the project says removing legacy access to existing endpoints is not planned now but may change.
Where pyproject and the README disagree on Python
Packaging metadata and prose point at two different floors. `pyproject.toml` declares:
name = "stripe"
version = "16.0.0"
requires-python = ">=3.9"and the Requirements section states that Python 3.9+ is supported. The classifier list, an excerpt, still advertises two versions the install check rejects:
classifiers = [
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.12",
]Classifiers are display metadata, so a package index page can answer 3.7 while the resolver refuses the install. The `version` field reads 16.0.0 while v16.1.0a1 and v16.1.0b1 were published on 2026-10-01, so a fresh clone pins the lower of those two lines.
The deprecation note below it disagrees a third time. It says that from version 6.0.0 the packages dropped Python 2.7, that 5.5.0 was the last version supporting it, and then tells readers to move to Python 3.6+ for new features and security updates. One file therefore states three floors: 3.6+ in the note, 3.9+ in the install check, 3.7 in the classifiers. Pin your runtime from `requires-python`.
Installing from PyPI and making the first list call
The package sits on PyPI under the name `stripe`, and the documented way to take it into an existing environment is:
pip install --upgrade stripeFrom a checkout of the repository, `python -m pip install .` installs the tree in place instead. The runtime floor is Python 3.9, and one extra is worth knowing about: `pip install stripe[async]` adds `httpx`, so the async paths work without further setup from you.
A first call needs a secret key and nothing else:
from stripe import StripeClient
client = StripeClient("sk_test_...")
# list customers
customers = client.v1.customers.list()
# print the first customer's email
print(customers.data[0].email)
# retrieve specific Customer
customer = client.v1.customers.retrieve("cus_123456789")
# print that customer's email
print(customer.email)`customers.data[0].email` is attribute access on the first element of the returned list, and the second call takes a resource id. If you run somewhere that offers only urlfetch, that is what the `http_client` option is for. Unsuccessful requests raise exceptions whose class reflects the sort of error that came back, and the README points at the Api Reference for which classes to catch rather than reproducing the list here.
to_dict returns a copy, so dict edits vanish
Conversion helpers are where older code breaks. `obj.to_dict()` walks the entire tree into native Python values, `obj.to_dict(recursive=False)` stops at the top level and leaves nested resources as objects, and `obj.to_dict(for_json=True)` makes every value JSON-serializable. That last flag changes types quietly: an amount held as `Decimal('7.89')` comes back as the string `7.89`, so a comparison against a float further down your code stops matching.
Take the example object the documentation uses:
obj = Customer(id='cus_123', subscription=Subscription(id='sub_456', amount=Decimal('7.89'))Then note the sentence that decides how much rework an upgrade needs: `.to_dict()` returns a copy, so changes to the dict are not reflected in `obj`. Code written before v15 could assign into that dict and believe the resource itself had changed. The same code now edits a throwaway copy and tells Stripe nothing at all. Reading is unaffected. Writing has to build a parameter dict and send it as arguments.
For serialization, `str(obj)` produces the same JSON string as the `for_json=True` form, which matters when a payload is hashed or signed.
Choosing an HTTP client, and the unfinished example
Transport is pluggable through the `http_client` argument, which accepts `urlfetch`, `requests`, `pycurl`, or `urllib`:
client = StripeClient("sk_test_...", http_client=stripe.UrlFetchClient())That snippet is the only concrete example on offer, and it is unfinished: the block ends mid-token after a stray two-letter fragment, and no import of the `stripe` module appears above it. You cannot paste it into a file unchanged. What the option buys is control over pooling and adapter behaviour, which matters on runtimes where urlfetch is all there is, and what it costs is a dependency choice made before your first request rather than after it.
One detail decides the dependency argument:
dependencies = [
"typing_extensions >= 4.7.0",
"requests >= 2.20",
]`requests >= 2.20` is a hard runtime requirement rather than an extra, so a service that already uses requests gains nothing by avoiding the SDK. `httpx` arrives only with the async extra. Neither bound is capped from above, so a major bump in either library is yours to absorb in tests rather than something the metadata holds back.
Generated resources, codegen version files, and what a clone runs
The package is generated code, and the top level of the repository shows how it is tracked: `CODEGEN_VERSION` and `OPENAPI_VERSION` sit next to `deps/` and the `stripe/` package, so the API surface you get is tied to a spec version recorded in the tree. A `.git-blame-ignore-revs` file is there too, the usual sign that commits were rewritten wholesale after a repository-wide reformat.
Local work runs through recipes in `justfile`. `prepare` chains format, lint, typecheck and test, and the three static checks look like this:
python -m flake8 --show-source stripe tests
PYRIGHT_PYTHON_IGNORE_WARNINGS=1 pyright
ruff format . --check > /dev/null`PYRIGHT_PYTHON_IGNORE_WARNINGS=1` exists to suppress version update warnings during the type check, which tells you how much churn a dependency bump creates. Packaging runs on `flit_core >=3.11, <4` and ends with a twine check of whatever `python -m build` produced.
One line blocks a plain clone: `justfile` opens with an optional import of `../sdk-codegen/utils.just`, a sibling directory outside this repository, so every recipe defined there is missing for anyone working outside that layout.
The last push was on 2026-10-06. The package is MIT licensed, and `pyproject.toml` lists `LICENSE` under `license-files`; that file holds the licence text governing redistribution, and nothing here is a legal reading of it.
When a raw request beats the typed StripeClient
The repository ships its own counterexample to the typed client. `examples/raw_request.py` and `examples/proxy.py` sit beside `examples/webhooks.py`, `examples/oauth.py` and `examples/meter_event_stream.py`, so writing the HTTP call yourself is a path the project documents rather than one the library forbids.
The difference shows up in what lands in your own code. Raw requests hand back plain dictionaries and leave error handling to you, with no dynamic class construction and no per-call option plumbing. The client hands back resources whose fields survive API version drift, exception classes that reflect the sort of failure Stripe returned, and a per-call `stripe_version` that pins the API date on the request itself.
For verifying a webhook signature or running a one-off admin script, the raw request stays shorter and easier to read. For a service that touches a lot of endpoints, pins API dates per call, or passes objects into code that branches on fields, hand-rolling parameter encoding and option plumbing duplicates work the library has already done.
Editorial conclusion
Adopt stripe-python when a service talks to the Stripe API from Python and you want typed resources plus per-call API version pinning. Do not adopt it for a single webhook endpoint, where the shipped raw request example is less code. Verify three things first: your runtime is 3.9 or newer, your call sites use StripeClient rather than the legacy global pattern, and no code mutates a to_dict result expecting Stripe to see the change. The legacy pattern still works today, and new endpoints will not be reachable through it.
Frequently asked questions
Does stripe-python still support Python 2.7?
No. The packages stopped supporting Python 2.7 with version 6.0.0, and 5.5.0 was the last version that supported it. The install metadata sets requires-python to >=3.9.
What is the difference between StripeClient and the legacy pattern in stripe-python?
StripeClient was introduced in v8 and keeps the secret key on the client, with resources reached through the v1 namespace. The legacy pattern sets a module-level stripe.api_key, still works, and will be marked as deprecated soon, after which new endpoints will be reachable only through StripeClient.
Why did my code break after upgrading stripe-python?
In v15, StripeObject stopped inheriting from dict, so code that treated a response as a dict no longer behaves the same way. to_dict returns a copy, and changes to that dict are not reflected back in the object, so edits there never reach Stripe.
How do I make a single stripe-python request as a connected account?
Pass the options argument to the call and set stripe_account inside it. The same dict can carry api_key and stripe_version, so one request can act for a connected account and pin an API date at the same time.
Which HTTP clients can stripe-python use?
The http_client option accepts urlfetch, requests, pycurl, or urllib. requests >= 2.20 is a hard runtime dependency in any case, and httpx is added only by the async extra.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/stripe-stripe-python)