-
Notifications
You must be signed in to change notification settings - Fork 455
Description
Background
The objects you get back from the Stripe API all share a common base class: StripeObject
(source). It holds the raw API response and has helpers for reading, writing, and updating data using a convenient dot notation: obj.name.whatever
.
For many years, StripeObject
has inherited from dict
, which means you get dict
-like behavior "for free": len(obj)
shows how many keys are in the StripeObject
, for key in obj
iterates through those keys, etc.
While implementing all of the dict
methods confers certain advantages when working with maps of data, there are also drawbacks. Most notably, any API response fields that share a name with a built-in method are inaccessible with dot notation:
o = Subscription() # an API response
o.whatever # works
o.whatever.sub_property # also fine
o.items # builtin method of `dict`
o.items[0] # error, method isn't subscriptable
o['items'] # list of StripeObjects, unintuitive
Over the years, we've gotten many issues filed about this counterintuitive behavior. But, we're hesitant to make breaking changes for behavior that's existed for so long. To better understand the implications of these changes, we're soliciting developer feedback to make sure we're serving your needs well.
Proposed Solution
Our proposal is that StripeObject
would not inherit from anything. As a result, it would lose its dict-like behavior (len(obj)
, obj.items()
, {**obj}
, json.dumps(obj)
etc) but retain property access via dot notation. Most documented examples don't take advantage (or even mention) that StripeObject
s are dictionaries, so we're hoping to not lose much functionality here.
In the interest of minimizing the impact of this change, we'd add a StripeObject.as_dict()
(name not final) method to return the underlying dictionary and enable all the behavior from before. As a result, the migration to keep existing behavior would be clear and relatively simple: call that method when you need dict
-specific functionality.
Impact
As a result of no longer being a dict
, any. This includes (but is not limited to):
- Passing the properties of a
StripeObject
to a function usingsome_func(**stripe_obj)
- Dumping a
StripeObject
to json:json.dumps(stripe_obj)
would need thedefault=vars
kwarg isintance(obj, dict)
checks would change behavior, if you have any of those
Alternative Solution
We have an existing mechanism to separate the name of the class property from the API value. We already use this for tax.Registration.CountryOptions.Is
(docs) because tax.registration.country_options.is
is a SyntaxError
; we useis_
instead.
We could do the same thing for subscription.items
. .items
would continue to be dict.items()
and .items_
would be the list of SubscriptionItem
s. The type annotations would indicate this and the existing obj['items']
approach would continue to work.
We're not thrilled with this because it doesn't match the data you get from the API and it's an extra thing to think about and remember. But, in a world where most users write code with rich typing support, it may not be a big deal. Plus, this wouldn't be a breaking change, which is useful for reducing general churn and toil.
Our Questions for You:
- What code, if any, do you have that takes advantage of the fact that StripeObject is a dict?
- If you called
some_obj.as_dict()
and made modifications to that dictionary, would you expect those changes to be reflected insome_obj
?. Does your answer change if the method is calledto_dict()
instead? - Would you prefer that we fixed the
items
bug using the alternative solution mentioned above and kept the currentdict
inheritance? Why or why not?
Feel free to provide any other suggestions, comments, or use cases that you think can be helpful. And as always, thank you for helping us make the Python SDK the best it can be!