10 Covenants
From charlesreid1
Ten Covenants of API Evolution: https://emptysqua.re/blog/api-evolution-the-right-way/
Contents
- 1 First Covenant: Avoid Bad Features
- 2 Second Covenant: Minimize Features
- 3 Third Covenant: Keep Features Narrow
- 4 Fourth Covenant: Mark Experimental Features "Provisional"
- 5 Fifth Covenant: Delete Features Gently
- 6 Sixth Covenant: Maintain a Changelog
- 7 Seventh Covenant: Choose a Version Scheme
- 8 Eighth Covenant: Write an Upgrade Guide
- 9 Ninth Covenant: Add Parameters Compatibly
- 10 Tenth Covenant: Change Behavior Gradually
- 11 Summary
- 12 Flags
First Covenant: Avoid Bad Features
Your goal is not to provide features that can be used to bite users, it's to provide features that work and don't have weird subtle behaviors
"It would have been better if this bad feature were never implemented at all."
Second Covenant: Minimize Features
To avoid bad features, add few features in general!
Features are like children: conceived in a moment of passion, they must be supported for years.
Third Covenant: Keep Features Narrow
You don't want a feature that, if abused, breaks everything ever and changes the very foundational nature of what your library does.
Fourth Covenant: Mark Experimental Features "Provisional"
Don't get locked into a dumb feature - if you aren't sure you really want to keep it, mark it as experimental/provisional. Next release, keep what works (mark it official) and discard what doesn't.
Fifth Covenant: Delete Features Gently
This is a promise responsible creators make
Follow a set of steps - add the new method, deprecate the old method (use Python warnings module), There’s a few steps involved in deleting a feature gently. Starting with a lizard that walks with its legs, you first add the new method, “slither”. Next, deprecate the old method.
Modify unittests so that warnings (which will not normally cause tests to fail) will become exceptions (which will cause the tests to fail):
> python3 -Werror::DeprecationWarning script.py
Sixth Covenant: Maintain a Changelog
keep a changelog
announce when a feature is added or deprecated or deleted
Seventh Covenant: Choose a Version Scheme
Eighth Covenant: Write an Upgrade Guide
example:
Upgrade to 1.1 and test your code with: python -Werror::DeprecationWarning Now it's safe to upgrade.
The Twisted project’s Compatibility Policy explains this beautifully:
“The First One’s Always Free”
Any application which runs without warnings may be upgraded one minor version of Twisted.
In other words, any application which runs its tests without triggering any warnings from Twisted should be able to have its Twisted version upgraded at least once with no ill effects except the possible production of new warnings.
Ninth Covenant: Add Parameters Compatibly
add new parameters with default values that preserve the original behavior
Over time, parameters are the natural history of your function’s evolution.
Parameters are listed oldest first, each with a default value.
Library users can pass keyword arguments to opt in to specific new behaviors, and accept the defaults for all others.
I like a technique like this one that detects whether any user’s code is relying on this parameter:
# Your library code. _turbo_default = object() def move(direction, mode='slither', turbo=_turbo_default, extra_sinuous=False, hail_lyft=False): if turbo is not _turbo_default: warnings.warn( "'turbo' is deprecated", DeprecationWarning, stacklevel=2) else: # The old default. turbo = False
But this only raises a warning
Zen of Python: "Errors should never pass silently."
The best way to protect your users is with Python 3’s star syntax, which requires callers to pass keyword arguments.
# Your library code. # All arguments after "*" must be passed by keyword. def move(direction, *, mode='slither', turbo=False, extra_sinuous=False, hail_lyft=False): # ... # A user's application, poorly-written. # Error! Can't use positional args, keyword args required. move('north', 'slither', False, True)
With the star in place, this is the only syntax allowed:
# A user's application. move('north', extra_sinuous=True)
Tenth Covenant: Change Behavior Gradually
Here are the steps:
- Add a flag to opt in to the new behavior, default False, warn if it’s False
- Change default to True, deprecate flag entirely
- Remove the flag
If you follow semantic versioning, the versions might be like so:
- 1.0 - no flag, expect old behavior
- 1.1 - add flag, default false
- 2.0 - change default to true, handle new behavior
- 3.0 - remove flag, handle new behavior
Summary
Evolve Cautiously
- Avoid Bad Features
- Minimize Features
- Keep Features Narrow
- Mark Experimental Features “Provisional”
- Delete Features Gently
Record History Rigorously
- Maintain a Changelog
- Choose a Version Scheme
- Write an Upgrade Guide
Change Slowly and Loudly
- Add Parameters Compatibly
- Change Behavior Gradually