-
Notifications
You must be signed in to change notification settings - Fork 470
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, unintuitiveOver 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 StripeObjects 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
StripeObjectto a function usingsome_func(**stripe_obj) - Dumping a
StripeObjectto json:json.dumps(stripe_obj)would need thedefault=varskwarg 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 SubscriptionItems. 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
itemsbug using the alternative solution mentioned above and kept the currentdictinheritance? 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!