Skip to content

RFC: What if StripeObject didn't inherit from dict? #1454

@xavdid-stripe

Description

@xavdid-stripe

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 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):

  1. Passing the properties of a StripeObject to a function using some_func(**stripe_obj)
  2. Dumping a StripeObject to json: json.dumps(stripe_obj) would need the default=vars kwarg
  3. 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:

  1. What code, if any, do you have that takes advantage of the fact that StripeObject is a dict?
  2. If you called some_obj.as_dict() and made modifications to that dictionary, would you expect those changes to be reflected in some_obj?. Does your answer change if the method is called to_dict() instead?
  3. Would you prefer that we fixed the items bug using the alternative solution mentioned above and kept the current dict 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!

See Also

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions