Three releases matter here. 8.0.0 (January 25, 2024) introduced StripeClient and a service-based call pattern. 12.5.0 (August 27, 2025) copied every v1 service under client.v1, and 13.0.0 (September 30, 2025) deprecated the old client.customers style accessors. 15.0.0 (March 25, 2026) made StripeObject stop inheriting from dict, which breaks .get() on every response whichever call style you use. The current major is 16.0.0. Stripe's README says the legacy pattern will be marked deprecated, after which new API endpoints will only be reachable through StripeClient. This guide follows Stripe's own migration guides and changelog, linked at the end.
What changed
- A
StripeClientinstance replaces the globalstripe.api_keyand other module settings. - Static resource methods become service methods:
stripe.Customer.create(...)becomesclient.v1.customers.create(...). - Request parameters go in a
paramsdict and request options such asstripe_accountgo in anoptionsdict. modify()is calledupdate()on services, and nested resources get their own services.stripe.Webhook.construct_eventbecomesclient.construct_event.- Code already on
StripeClientmust add.v1:client.customersis deprecated since 13.0.0. - From 15.0.0,
StripeObjectis not adict:.get(),.keys()anddict(obj)no longer work, anddecimal_stringfields areDecimal.
Migration steps
Upgrade and read the pinned API version
pip install --upgrade stripeEach stripe-python major pins a Stripe API version: 15.0.0 pins
2026-03-25.dahliaand 16.0.0 pins2026-09-30.endive. Upgrading the library can therefore change API behavior as well as Python syntax. Read the Stripe API changelog for each version you cross. 15.0.0 also dropped Python 3.7 and 3.8.Create a client instead of setting globals
Before
import stripe stripe.api_key = os.environ["STRIPE_SECRET_KEY"] stripe.max_network_retries = 3After
from stripe import StripeClient client = StripeClient( os.environ["STRIPE_SECRET_KEY"], max_network_retries=3, )Former globals such as
stripe_version,max_network_retriesandproxybecome keyword arguments on the constructor. Because there is no global state, you can run several clients with different keys side by side, and tests can pass a fake client instead of patching module functions.Move calls to services, with params and options
Before
customer = stripe.Customer.create( email="jenny.rosen@example.com", stripe_account="acct_123", )After
customer = client.v1.customers.create( params={"email": "jenny.rosen@example.com"}, options={"stripe_account": "acct_123"}, )Resource methods mixed API parameters and request options in one set of keyword arguments. Services keep them apart:
paramsfor what goes to the API,optionsforapi_key,stripe_account,stripe_versionandidempotency_key.Rename modify to update, and move nested calls
Before
stripe.Customer.modify("cus_123", description="VIP") stripe.Customer.list_balance_transactions("cus_123", limit=3)After
client.v1.customers.update("cus_123", params={"description": "VIP"}) client.v1.customers.balance_transactions.list("cus_123", params={"limit": 3})Pass IDs positionally, as above. Since 16.0.0, path parameters such as the customer ID must be positional on service methods; passing them by keyword is no longer supported.
Construct webhook events through the client
Before
event = stripe.Webhook.construct_event(payload, sig_header, secret)After
event = client.construct_event(payload, sig_header, secret)Add .v1 to existing StripeClient code
Before
customers = client.customers.list()After
customers = client.v1.customers.list()Stripe added a
v2namespace for its newer APIs, so the original APIs now live underv1. The top-level accessors still work as of 16.0.0, but they have been deprecated since 13.0.0, Stripe says they will be removed in a future release, and editors flag them.Stop treating Stripe objects as dicts
Before
name = customer.get("name", "unknown") data = dict(customer) for key in customer.keys(): ...After
name = getattr(customer, "name", "unknown") data = customer.to_dict() for key in customer.to_dict().keys(): ...- Attribute access,
customer["name"],"name" in customerandstr(customer)still work. - For JSON, use
str(obj)orobj.to_dict(for_json=True).to_dict()returns a copy; changing it does not change the object. .items()no longer meansdict.items(): on a subscription,subscription.itemsis the subscription's line items.to_dict_recursive()is removed (useto_dict()), andstripe_idis removed (useid).decimal_stringfields such asunit_amount_decimalareDecimalnow, so compare withDecimal("9.99"), not"9.99".
- Attribute access,
How to find every affected call
ast-grep finds the call styles that need to change, including calls split across lines:
sg run --pattern 'stripe.api_key = $KEY' --lang python
sg run --pattern 'stripe.$RESOURCE.create($$$ARGS)' --lang python
sg run --pattern 'stripe.$RESOURCE.modify($$$ARGS)' --lang python
sg run --pattern 'stripe.$NS.$RESOURCE.create($$$ARGS)' --lang python
sg run --pattern 'stripe.Webhook.construct_event($$$ARGS)' --lang python
sg run --pattern '$CLIENT.customers.$METHOD($$$ARGS)' --lang python$RESOURCE matches one name, so stripe.checkout.Session.create needs the $NS pattern. Repeat the create and modify patterns for retrieve, list and delete, and the last pattern for each service you use. The dict-style reads from step 7 are harder: .get() on a Stripe object looks exactly like .get() on a dict. Search for .get(, .keys() and dict( on variables that hold Stripe responses, and rely on tests that exercise those code paths.
Testing the change
- Run mypy or pyright. stripe-python ships type hints, and since 13.0.0 deprecated
client.customersstyle calls are flagged in your editor. - Replace tests that patch
stripe.Customer.createwith a fake or mocked client passed into your code. stripe-mock, Stripe's mock API server, is useful for running real requests in CI. - Run your payment flows end to end in test mode with a test key, including one Connect call with
stripe_accountif you use Connect. - Replay webhooks with the Stripe CLI (
stripe listenandstripe trigger) to confirmclient.construct_eventverifies signatures and your handlers read fields correctly.
Automating it
Stripe publishes @stripe/sdk-migrator, which uses ast-grep. For the v1 namespace change, preview with npx @stripe/sdk-migrator --language python --migration v1-namespace -d <path> and add --execute to apply; add --untyped for code without type hints. Stripe calls it best effort, especially in dynamic code. At the time of writing its README documents only the v1-namespace migration, so moving legacy stripe.Customer calls to StripeClient, and the v15 dict change, are manual.
Versyka handles the whole path: it finds call sites deterministically with ast-grep, rewrites them along with the code that reads the results, then runs your repository's own type check and pytest in an isolated virtualenv inside a disposable sandbox. It opens a pull request only if those pass.
Was this guide helpful?
Check your own repository
Paste your package.json, requirements.txt or pyproject.toml into the free upgrade check to see which API SDKs are behind, with no account. Or let Versyka run this migration on one repository and open a pull request that has already passed your tests.