Give Codeship a try

Want to learn more?

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:

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:

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.

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:

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.

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:

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:

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:

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.

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles. Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.

We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.