# Standard way to deprecate a keyword argument

**URL:** https://discourse.julialang.org/t/standard-way-to-deprecate-a-keyword-argument/100534
**Category:** General Usage
**Tags:** question
**Created:** [June 18, 2023, 11:02pm UTC](https://discourse.julialang.org/t/standard-way-to-deprecate-a-keyword-argument/100534 "2023-06-18T23:02:23Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [June 18, 2023, 11:02pm UTC](https://discourse.julialang.org/t/standard-way-to-deprecate-a-keyword-argument/100534/1 "2023-06-18T23:02:23Z")

</div>

Is there a simple and standard way to deprecate a keyword argument? From reading through past threads it sounds like you need to do this manually, e.g., [Deprecate keyword argument](https://discourse.julialang.org/t/deprecate-keyword-argument/33740). But surely there’s a nice macro for this somewhere?

Here’s the syntax I am looking for to deprecate `a` in favor of the new kw `b`:

```julia
function f(; @deprecate_kw(a, b=2))
    return b
end

f(a=2) # Works, but raises a depwarn
f(b=2) # Also works

```

Here’s a simple attempt which works without needing to wrap the entire function signature. However, I don’t like this because:

1. I still need to pass `deprecated_kw=nothing`.
  - _Aside: Is there any way to generate multiple keywords using a macro that is used inline in the function signature?_

2. The `depwarn` doesn’t know the function.
  - _Aside: I tried calling `stacktrace(backtrace())` from within the kwcall to get the function name, but it seemed too hacky._

3. Code analyzers like the one used in VSCode do not know about the new keyword.

> **(expand)**
>
> ```julia
> macro deprecate_kw(deprecated_kw, new_kw)
> return _deprecate_kw(deprecated_kw, new_kw) |> esc
> end
> 
> function _deprecate_kw(deprecated_kw::Symbol, new_kw::Expr)
> @assert(new_kw.head == :(=), "Must pass a simple assignment expression.")
> default_value = new_kw.args[2]
> depwarn_string = "The keyword argument $(deprecated_kw) is deprecated in favor of $(new_kw.args[1])."
> new_kw.args[2] = quote
> if $deprecated_kw != nothing
> # Get function name to put in depwarn:
> Base.depwarn($depwarn_string, :nothing)
> $deprecated_kw
> else
> $default_value
> end
> end
> return Expr(:kw, new_kw.args[1], new_kw.args[2])
> end
> 
> ```

Then I can use this with:

```julia
function f(;
    a=nothing, @deprecate_kw(a, b=2),
    c=nothing, @deprecate_kw(c, d=3),
)
    return b + d
end

```

with outputs (under `julia --depwarn=yes`):

```julia
julia> f(a=2)
┌ Warning: The keyword argument a is deprecated in favor of b.
│ caller = ip:0x0
└ @ Core :-1
5

julia> f(b=2)
5

```

Is there a better way to do this? Or is manually swapping argument values in the function body the only way?

* * *

> **Here's the macro expansion for clarity:**
>
> ```julia
> julia> @macroexpand @deprecate_kw(a, b=2)
> :($(Expr(:kw, :b, quote
> if a != nothing
> Base.depwarn("The keyword argument a is deprecated in favor of b.", :nothing)
> a
> else
> 2
> end
> end)))
> 
> ```

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [June 18, 2023, 11:16pm UTC](https://discourse.julialang.org/t/standard-way-to-deprecate-a-keyword-argument/100534/2 "2023-06-18T23:16:50Z")

</div>

I’d prefer to not have to wrap the entire signature, but I guess it might be necessary for (1) the depwarn to know the function, and (2) to allow for macros which return multiple keywords. So maybe something like

```julia
@deprecate_kws (b=a, d=c) function f(; b=2, d=3)
    return b + d
end

```

could also be an option, though it’s not very elegant…

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [June 19, 2023, 12:01am UTC](https://discourse.julialang.org/t/standard-way-to-deprecate-a-keyword-argument/100534/3 "2023-06-19T00:01:58Z")

</div>

Okay, here’s a way to get the desired syntax above to work:

```julia
using MacroTools

macro deprecate_kws(deprecations, def)
    return esc(_deprecate_kws(deprecations, def))
end

function _deprecate_kws(deprecations, def)
    @assert(
        deprecations.head == :tuple,
        "First argument to `deprecate_kws` must be passed as a tuple, like `(new_kw1=old_kw1, new_kw2=old_kw2)`"
    )
    sdef = splitdef(def)
    func_symbol = Expr(:quote, sdef[:name]) # Double quote for expansion

    new_symbols = [_get_symbol(k.args[1]) for k in deprecations.args]
    deprecated_symbols = [_get_symbol(k.args[2]) for k in deprecations.args]
    symbol_mapping = Dict(new_symbols .=> deprecated_symbols)

    # Add deprecated kws:
    for deprecated_symbol in deprecated_symbols
        pushfirst!(sdef[:kwargs], Expr(:kw, deprecated_symbol, :nothing))
    end

    # Update new symbols to use deprecated kws if passed:
    for (i, kw) in enumerate(sdef[:kwargs])
        new_kw = kw.args[1]
        default = kw.args[2]
        _get_symbol(new_kw) in deprecated_symbols && continue
        !(_get_symbol(new_kw) in new_symbols) && continue
        deprecated_symbol = symbol_mapping[_get_symbol(new_kw)]
        depwarn_string = "Keyword argument `$(deprecated_symbol)` is deprecated. Use `$(_get_symbol(new_kw))` instead."
        new_kwcall = quote
            if $deprecated_symbol !== nothing
                Base.depwarn($depwarn_string, $func_symbol)
                $deprecated_symbol
            else
                $default
            end
        end
        sdef[:kwargs][i] = Expr(:kw, new_kw, new_kwcall)
    end

    return combinedef(sdef)
end

# This is used to go from a::Int to a
_get_symbol(e::Expr) = first(map(_get_symbol, e.args))
_get_symbol(e::Symbol) = e

```

With this we can write:

```julia
@deprecate_kws (b=a, d=c) function f(; b::Int=2, d=3)
    return b + d
end

```

> **which expands to (click me):**
>
> ```julia
> function f(; c = nothing, a = nothing, b::Int = begin
> if a !== nothing
> Base.depwarn("Keyword argument `a` is deprecated. Use `b` instead.", :f)
> a
> else
> 2
> end
> end, d = begin
> if c !== nothing
> Base.depwarn("Keyword argument `c` is deprecated. Use `d` instead.", :f)
> c
> else
> 3
> end
> end)
> return b + d
> end
> 
> ```

this has everything work as you might expect:

```julia
julia> f(a=2)
┌ Warning: Keyword argument `a` is deprecated. Use `b` instead.
│ caller = top-level scope at REPL[5]:1
└ @ Core REPL[5]:1
5

julia> f(b=2)
5

julia> f(d=2)
4

julia> f(c=3)
┌ Warning: Keyword argument `c` is deprecated. Use `d` instead.
│ caller = top-level scope at REPL[8]:1
└ @ Core REPL[8]:1
5

```

Even the `::Int` still works:

```julia
julia> f(b=2.0)
ERROR: TypeError: in keyword argument b, expected Int64, got a value of type Float64
Stacktrace:
 [1] top-level scope
   @ REPL[9]:1

```

though the error is a bit weird for `a=2.0` (at least it throws an error though)

> **(expand)**
>
> ```julia
> julia> f(a=2.0)
> ┌ Warning: Keyword argument `a` is deprecated. Use `b` instead.
> │ caller = top-level scope at REPL[10]:1
> └ @ Core REPL[10]:1
> ERROR: MethodError: no method matching var"#f#13"(::Nothing, ::Float64, ::Float64, ::Int64, ::typeof(f))
> 
> Closest candidates are:
> var"#f#13"(::Any, ::Any, ::Int64, ::Any, ::typeof(f))
> @ Main REPL[4]:1
> 
> Stacktrace:
> [1] top-level scope
> @ REPL[10]:1
> 
> ```

Is this approach the optimal way of solving this sort of thing? Let me know if you know of something better.

One concern I have is that this approach might not be type stable.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [June 22, 2023, 7:55pm UTC](https://discourse.julialang.org/t/standard-way-to-deprecate-a-keyword-argument/100534/4 "2023-06-22T19:55:22Z")

</div>

Okay I have since made a mini package for this:

[GitHub - MilesCranmer/DeprecateKeywords.jl: Macro for deprecating keyword parameters](https://github.com/MilesCranmer/DeprecateKeywords.jl/tree/master).

Here’s the description on the readme:

* * *

# DeprecateKeywords.jl

[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://astroautomata.com/DeprecateKeywords.jl/dev/) [![Build Status](https://github.com/MilesCranmer/DeprecateKeywords.jl/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/MilesCranmer/DeprecateKeywords.jl/actions/workflows/CI.yml?query=branch%3Amaster) [![Coverage](https://coveralls.io/repos/github/MilesCranmer/DeprecateKeywords.jl/badge.svg?branch=master)](https://coveralls.io/github/MilesCranmer/DeprecateKeywords.jl?branch=master)

DeprecateKeywords defines a macro for keyword deprecation. For example, let’s say we wish to deprecate the keyword `old_kw1` in favor of `new_kw1`, and likewise for `old_kw2`.

```julia
using DeprecateKeywords

@deprecate_kws function foo(;
    new_kw1=2,
    new_kw2=3,
    @deprecate(old_kw1, new_kw1),
    @deprecate(old_kw2, new_kw2)
)
    new_kw1 + new_kw2
end

```

With this, we can use both the old and new keywords. If using the old keyword, it will automatically be passed to the new keyword, but with a deprecation warning.

```julia
julia> foo(new_kw1=1, new_kw2=2)
3

julia> foo(old_kw1=1, new_kw2=2)
┌ Warning: Keyword argument `old_kw1` is deprecated. Use `new_kw1` instead.
│ caller = top-level scope at REPL[5]:1
└ @ Core REPL[5]:1
3

```

(The warning uses `depwarn`, so is only visible if one starts with `--depwarn=yes`)

* * *

Internally, the `@deprecated_kws` reads through the signature and replaces `@deprecated(a, b)` (which is not currently usable with keywords) with some logic at the beginning of the kwargs. Here is what this code expands to:

```julia-auto
:(function foo(; old_kw2 = nothing, old_kw1 = nothing, new_kw1 = begin
                  #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:75 =#
                  if old_kw1 !== nothing
                      #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:76 =#
                      Base.depwarn("Keyword argument `old_kw1` is deprecated. Use `new_kw1` instead.", :foo)
                      #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:77 =#
                      old_kw1
                  else
                      #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:79 =#
                      2
                  end
              end, new_kw2 = begin
                  #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:75 =#
                  if old_kw2 !== nothing
                      #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:76 =#
                      Base.depwarn("Keyword argument `old_kw2` is deprecated. Use `new_kw2` instead.", :foo)
                      #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:77 =#
                      old_kw2
                  else
                      #= /Users/mcranmer/Documents/DeprecateKeyword.jl/src/DeprecateKeywords.jl:79 =#
                      3
                  end
              end)
      #= REPL[3]:1 =#
      #= REPL[3]:7 =#
      new_kw1 + new_kw2
  end)

```

One thing that is missing is a check for both kwargs being set (right now it silently takes the new kw)
