The Straight Dope on Deprecations
The road to stability is paved with good deprecations. A deprecation is a warning message that tells a user they’re using some piece of code or interface that will go away soon. In this post, we’ll peel back the seemingly simple veneer of deprecations, and we will learn when and how to use deprecations effectively.
There Is No Going Back
When I first started programming, I ran into some functions called something like myFunction()
and myFunction2()
.
When I asked about them, it turned out that the original function didn’t do everything that the users needed, so they introduced a second one. Everyone on the project knew not to use myFunction
and instead only ever used myFunction2
. I thought that was crazy — what happens in the future when we have all the way up to myFunction42()
? Sure, the old function worked, but what happens when you need to bugfix one function? Do you apply it to all of them? How did people know which was the latest version they were supposed to use? One answer could be deprecations.
DEPRECATION: You are using deprecated function `myFunction()` please use `myFunction42()` instead projects/myProject/main.c:23
Here you can see the three main parts of a deprecation. We need some way to easily identify or grep for these, so we use a bold DEPRECATION
. Then we tell the user the thing they are using currently:
You are using deprecated function `myFunction()`
We then tell them what they should be using instead:
please use `myFunction42()` instead
The most important part is the last thing. We tell the user where the old interface is being invoked:
projects/myProject/main.c:23
It’s in the file projects/myProject/main.c
on line 23.
There is one other crucial piece here that we’re missing — did it jump out at you? To be able to deprecate code, we need version numbers. I’ve written about SemVer for library maintainers and The Optimist’s Guide to Pessimistic Library Versioning. Generally you want to add deprecations to your software in a major (simpler?) version before you release any kind of breaking changes in a major version.
Deprecation != Deletion
Just because you deprecate something does not mean you can delete it. You always need an alternative interface that people can use right away.
If the old interface is not posing an undue maintenance burden, keep it around. Remember why your code is important in the first place — it’s because people use it. From a great talk by Loren Segal on “Ethical Deprecation”:
Your users are more important than your code […] It is why your library exists. If you have the choice between making your users’ lives unhappy or your code unhapoy, you should choose to make your code unhappy because it doesn’t care.
Consider what costs your users will have to go through when you are changing an API. There are also lots of examples of code from five-plus years ago that still works.
Loren maintains YARD which needs to be extremely stable. In fact, you could argue that the stability of YARD is one of its features. If you’re deprecating an interface that doesn’t pose a support burden — maybe you’re renaming things to be more consistent — consider whether or not you actually need to remove the old interface. Does it really cost you that much to keep an alias method around?
It might seem like a trivial thing for someone to use a new method name, but sometimes it’s not. What if you’re an administrator of a system that uses a CLI-based tool that reads from a specific file name? You, as an administrator, may have the ability to upgrade the CLI tool, but the file might come from a third party. You might not control the name. When we choose to remove an interface, we are choosing to give our users homework. The more popular your library is, the more careful you must be about backwards-incompatible changes.
That being said, I don’t think your code absolutely has to maintain 100-percent backwards compatibility. Sometimes you need to remove or change an interface. For example, if your API design is causing performance problems, or perhaps people misuse the interface and it’s impossible to detect, raise a helpful error.
One interface decision I see commonly that people regret is inheriting from a core class such as String or Hash. If you’ve done this and decide you didn’t like that decision, it will be almost impossible to retain true 100-percent backwards compatibility without serious code hacks.
Consider your users and use your best judgement before removing an interface. If you do delete something, the least you can do for your users is deprecate before removing code.
Protect Private Methods
If you’re going to change anything that isn’t fully backwards compatible with your public interface, you should deprecate. What exactly is your public interface?
I mostly code in Ruby these days, so I’ll be using that for examples from now on.
A simple way to think of your public interface is to look at your README or examples. Are there classes or modules that are introduced in those examples by your code? That’s part of your interface. What about methods? Definitely part of your interface.
You can declare methods as private in Ruby, which prevents them from being used outside of your code (except via public_send
). This communicates to someone that they are not part of the “official” interface they should be using.
private def render_helper(args) # code here end
Now when you use that method inside of your code it’ll work, but when someone else tries to call it directly in their code it’ll blow up. You can change this private method name without deprecating since it’s only used internally to your project.
Note: Changing a public method to a private method is a breaking change. Don’t do this without warning.
You can do something similar with constants inside of a class like this:
class Foo BAR = "bar" private_constant :BAR def bar puts BAR end end Foo.new.bar # => "bar" Foo::BAR # => NameError: private constant Foo::BAR referenced
This means that you can use that BAR
class from anywhere inside the Foo
context, but trying to call it directly will blow up. Unfortunately you can’t make a top-level class private and use it outside of that class. This means we still need a way to declare those things as an interface you shouldn’t be using.
To communicate when a class or module is private, different projects do different things. For example, Rails uses :nodoc:
to prevent documentation from being generated for that class.
class NullMail #:nodoc: def body; '' end def header; {} end
Other document systems such as YARD have other directives such as @private
.
# @private class InteralImplementation; end
Marking @private
or :nodoc:
is not so that your documentation can be cleaner or so you can get higher “documentation coverage.” You should only use the tags when an interface should not be publicaly consumed. The private_constant
is understood by YARD, so if you’re using that you don’t also have to mark private constants with a tag.
If you’re not using a code documentation system, you should consider adopting one. I like YARD and sdoc. If you’re not hiding an interface from being documented, then it’s fair game to be used by any developer.
Docs and Deprecations
Now that you’re deprecating and documenting your project, you can go the extra mile and let people know what interfaces are deprecated in your documentation. YARD has a tag for this: @deprecated
.
That seems like twice the work though. What’s the benefit?
Runtime deprecations are great when your code is running and when you’re able to see the output. What if the deprecated interface never gets called in test or development? What if you want an easy way to reference all the things that were deprecated in a specific version? By marking interfaces as deprecated in your documentation, you can solve all these problems.
One example is comparing all the things that were deprecated in the YARD codebase between 0.5.0 and 0.8.7.6:
$ yard diff yard-0.5.0 yard-0.8.7.6 --query '@deprecated' Added objects: YARD.load_plugins (lib/yard.rb:29) YARD::CLI::Yardoc#all_objects (lib/yard/cli/yardoc.rb:312) YARD::CONFIG_DIR (lib/yard.rb:11) YARD::Config::IGNORED_PLUGINS (lib/yard/config.rb:101) YARD::Docstring::META_MATCH (lib/yard/docstring.rb:60) YARD::Handlers::Ruby::StructHandlerMethods (lib/yard/handlers/ruby/struct_handler_methods.rb:6) YARD::Logger#warn_no_continuations (lib/yard/logging.rb:152) #...
While you’re already in your code adding the deprecation, it’s pretty trivial to add a tag to your docs as well. This will also help you for when you’re working with your CHANGELOG.
Implementing Deprecations
When Rails changed the method before_filter
to before_action
, they added a deprecation. Basically, “use this other method instead.” They use Active Support, which comes with a built-in way to generate consistent deprecations and the appropriate backtrace:
ActiveSupport::Deprecation.warn("Thing <x> changed to thing <y>, use it instead")
If you’re not using ActiveSupport
, you can write to Kernel.warn
manually. Or you can use something like the deprecation gem.
One thing to note is that deprecations generally don’t go to STDOUT
. Instead they go to STDERR
, so if you’re executing a program via a CLI and capturing output, you can prevent the warnings from interfering with the output you need.
Deprecating Other Interfaces
Start thinking about interfaces and deprecation notices early. The easier it is to deprecate something, the better the interface. Start thinking along the lines of “if I have to change this later, how will I tell my users?” while you’re writing your code, and it can help inform you.
Hash keys
Many interfaces such as Rack’s call
method are implemented by passing around a hash. This means that the keys in your hash are now your interface, and if you change one of them, you must deprecate it. How can you do that?
In Ruby, you can set a default value for a hash by using a proc. For example:
default_value_hash = Hash.new {|hash, key| hash[key] = "hey look a #{key}"} default_value_hash["foo"] = "bar" puts default_value_hash["foo"] # => "bar" puts default_value_hash["zoo"] # => "hey look a zoo"
Notice that we never initialized a "zoo"
hash key. Instead one was generated through our default proc. You can use this to create deprecations:
hash = Hash.new do |hash, key| if :prev == key Deprecation.warn("Key `:prev` is deprecated, please use `:previous` instead") hash[:previous] end end hash[:previous] = :back
Now when someone uses that old key, they’ll get a warning:
hash[:prev] # => DEPRECATION WARNING: Key `:prev` is deprecated, please use `:previous` instead # => :back
If you don’t have control over the creation of the hash, you can add on a default proc using Hash#default_proc=. You can also pre-allocate a proc in a constant if you’re worried about the performance cost of allocating lots of new procs.
In general, I try to stay away from hash-based APIs. While they’re simple to implement, deprecating keys is not always straight forward, and you can run into problems when someone expects a string key to be the same as a symbol or vice versa. Hashes work well for simple data, but in many cases a “value object” may be a better option.
I talk in depth about this concept in Hashie Considered Harmful. Also if you want to see an interface that is basically impossible to deprecate, take a look at OmniAuth’s use of Hashie.
Method arguments
Most people think of deprecation as a way of changing method names, but you can use it to safely change the method signature — that is, the inputs that the method accepts.
For a hash-based example from Rails:
if options.delete(:nothing) ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.") options[:body] = nil end
You can also use this for positional arguments if you’re going from two inputs to one or if you want to change the type of input you accept.
unless input.is_a?(Symbol) Deprecation.warn("Using a #{ input.class } for an input is deprecated please use a symbol") end
As a side note, I love Ruby 2.1+ named arguments and required named arguments that were introduced in 2.2. Previously, if you wanted to accept many different arguments, the easiest way was to use an options hash, so your method might look like this:
def my_method(options = {}) # ... end
This can fail in odd ways. For example, if a user passes in a key that is misspelled, they may not get any errors since most coders won’t actually check all keys being passed in. Compare this with named arguments:
def my_method(username: "schneems", dog: 'Dachshund', hobby: 'woodworking', city: 'Austin') # ... end my_method(name: "Richard") # => ArgumentError: unknown keyword: name
Now if you use :name
instead of :username
, you will get a failure, which is a good thing. Named arguments can be easier to deprecate than positional arguments. For example, if you wanted to deprecate before changing that interface, you could include a name key in the method signature and do something like this:
def my_method(username: "schneems", dog: 'Dachshund', hobby: 'woodworking', city: 'Austin', name: nil) if name Deprecation.warn("Argument key `:name` is deprecated, please use `:username` instead") username = name end # ... end
Once you finish the process of changing the interface and remove support for this older key, then the user will get an instructive error instead of a possible silent failure.
When Your Interface Is a Method
So far we’ve talked about how to deprecate methods that you own. What do you do when you’re deprecating someone else’s method signature? Sounds weird, but that’s what libraries like Rack do: You give them something that responds to the call
method and takes a hash. What if you wanted to add or change a positional argument?
One way to deprecate is by checking the arity
. For example, if your user has a module like this:
module MyCall def self.call(one_input) # ... end end
In your code, you receive that module and then use the call
method. You can see how many positional arguments it has:
puts MyCall.method(:call).arity # => 1
Sprockets has something similar in its processor interface, a support shim was added in to support both old and new interfaces when inputs with different arity are used.
Constants
Constants are generally used in code to indicate, well, things that are constant.
SPACE = " ".freeze
There are times when you might want to modify a constant; say, you’ve got a regex. It’s fine to improve the regex as long as it doesn’t change desired functionality. However if you changed the logic that the regex represented, you should deprecate the constant and put the new functionality in a new variable.
How do you deprecate a constant? It’s not pretty, but you can deprecate using const_missing
.
class Foo def self.const_missing(name) if :SPACE == name Deprecation.warn("Foo::SPACE is being removed use ' ' directly instead.") return " ".freeze else super end end end
You want to make sure you always delegate to super
in an else
statement so you don’t accidentally swallow when someone tries to use a constant that does not exist.
Use your best judgement when using another project’s constants. If the constant is defined, has docs, and is mentioned in the README, it’s safe to use, and you should expect a deprecation before it gets removed or modified. However, if you’re using a random constant that doesn’t look related to core functionality, you might want to consider copying the constant value and creating your own.
If you’re creating an interface that consumes a constant, you can use defined?
for deprecations.
def my_method(input) if defined?(INPUT_SEPARATOR) Deprecation.warn("Constant `INPUT_SEPARATOR` is being deprecated, please use `MY_INPUT_SEPARATOR` instead") # ... else # ... end end
This way you can detect the presence of the constant safely without raising an error.
Deprecation Warning: You Aren’t Deprecating Enough
Most libraries out in the wild don’t deprecate enough or at all. They don’t think their library is “big enough,” or they’re “pretty confident no one is using that interface,” or “it’s totally their problem for using it wrong.” They break interfaces without breaking a sweat.
If you absolutely positively must change an interface, let people know. Don’t just do it quietly and assume nobody will notice, because they will.
I know deprecations seem like a chore, but they’re a really really useful chore, like getting new tires for your car before they pop going 80 mph on the interstate. Adding deprecations will help you expose good API design and make you a better developer.
It also makes the experience of using your library across version upgrades significantly better. You might be surprised — I’ve added deprecations that I wasn’t totally sure were needed. It turns out they caught places in my own code where I was using an older interface. Without the notification, I could have lost hours hunting down the error after the upgrade. Deprecations: good for your users, good for you.
As far as I’m concerned, there is no such thing as deprecating too much. If something is changing, let your users know about it. You do run the risk of spamming your users or maybe accidentally detecting a “deprecation” even when they are using the correct interface.
To these worries, I say: Worry about adding deprecations now, and deal with these cases when they come up.
Make another minor release that fixes the frequency or detection logic. The only time I would say to be cautious when adding deprecations is with a section of code that is especially performance sensitive. Some of the techniques listed above aren’t the most performant. One thing you can do is to make sure that your code ships with a deprecation free alternative that people can use right away. If that’s not possible, go ahead and release a major version bump (or major version beta) without the deprecation, so people who need it can use the new interface without any performance penalty.
If you’re not a library maintainer (yet), there’s still room for you in this conversation. If you find an upgrade particularly tough, ask yourself what changed and why you weren’t notified about it before.
The easiest way to start contributing to a library is with the documentation. Deprecations are essentially documentation in code form, so they’re the second easiest thing you could contribute. If you’re not sure if a deprecation is warranted, open up PR and you can talk about it along with some code.
Much like versioning, deprecations seem simple: almost boring. However, there’s complex nuance under the surface, as well as a great reward in using them diligently. When we as a community embrace deprecations, we make our whole ecosystem more stable. That stability means we get to spend less time hunting down “what on earth broke the app” and more time building features and shipping code.
So if deprecations help buy us stability, and stability helps buy productivity, help your library contribute to productivity by adding deprecations.
Reference: | The Straight Dope on Deprecations from our WCG partner Richard Schneeman at the Codeship Blog blog. |