Passing a keyword argument containing a '-'

Is there some way I can use a hyphen in the name of a keyword argument as in the example below?

set_cookie("cookieName", "cookieValue", Max-Age=0, Expires="")

No. Hyphens are never valid characters in Python names. That includes function names, variable names, and argument names. Use an underscore instead.

Really, you should just follow the PEP 8 naming conventions; they’re standard across most Python code. That means for argument names, you should use all lowercase with underscores for spaces.

You could have the cookie attributes as a dictionary instead.

That is unfortunate because in the case of http cookies I want to pass optional attributes; one of which may be ā€œMax-Ageā€.

options = {
    "Max-Age": 0,
    "Expires": ""
}

set_cookie("cookieName", "cookieValue", **options)

@elis.byberi I really wouldn’t do this. **kwargs is intended to let you pass keyword arguments, so it seems like a bad idea to expect things that aren’t valid keyword arguments.

If you adopted this signature, anyone using set_cookie would have to create a dictionary and unpack it. You might as well save them the step of unpacking by taking a dictionary argument directly.

Yes, I did unpack it erroneously.

options = {
    "Max-Age": 0,
    "Expires": ""
}

set_cookie("cookieName", "cookieValue", options)  # not **options

You will need to format the cookie string later, so it is easier to pass the optional values in a dictionary and simply iterate over them, or use a similar approach.

My workaround for this is usually to allow both. Have an optional positional argument that takes a dictionary context, then allow **kwargs as overrides.

def set_cookie(__cookie: CookieTD | None = None, /, **kwargs: Unpack[CookieTD]):
    opts = __cookie or {}
    opts.update(kwargs)
    ... # do something with the resolved cookie options

Note that you will likely need to use a deepcopy if any of the options are mutable containers (list, set, dict, etc), but this pattern has been pretty useful for me in a ton of cases. Especially when I’m needing to allow per function/method overrides of a whole giant set of parameters. This lets me create some defaults that can be passed as the positional, then allow small changes to be passed as arbitrary keyword arguments.

Plus if the API has something that isn’t a valid name, you can still pass it by unpacking it from a dictionary (which is annoying, but better in my opinion than renaming stuff and causing confusion later).

Passing invalid names to **kwargs is a bit of a code smell, I know, but at runtime they’re just treated as a normal dictionary, so it’s up to you if you want to ignore it or not.

It’s explicitly ok. From 6. Expressions — Python 3.14.6 documentation :

When **expression is used, each key in this mapping must be a string. … A key need not be a Python identifier (e.g. "max-temp °F" is acceptable, although it will not match any formal parameter that could be declared).

Of course, there can’t be a matching parameter name (in the normal way, at least), so clearly the function signature has to have a ** formal parameter.

The ability to provide arguments by name in a call, using names not explicitly declared in the signature and the id=value syntax, is really just a convenience for loading a dictionary to give to the ** argument of a call. Non-identifier keys turn up in some numerical libraries.

In this particular context, it might be worth looking at http.cookies — HTTP state management — Python 3.14.6 documentation and passing one of those.

The hyphen means subtract so it’s not usable in a name.

a = b-c

So Max-Age is Max subtract Age.

That’s what I was referring to as the ā€˜code smell’. Mainly since I was using an unpacked TypedDict to hint the kwargs. Which means the user will possibly get confusing hints. I don’t think this is really an issue if you provide an alternative name for that situation or just require them to unpack a dictionary.

I’mve had a similar problem, where Json usually uses a-b when python uses a_b. I just created a dictutils module for my project, which (amongst other stuff) contained following code for converting Json dictionaries to Python dictionaries:

import typing

def _j2p(key: str) -> str:
    return key.replace("-", "_")

def json2py(mapping: dict[str, typing.Any], *, recursive: bool = True) -> dict[str, typing.Any]:
    new: dict[str, typing.Any] = {}
    for key, value in mapping.items():
        key = _j2p(key)
        if isinstance(value, dict) and recursive:
            value = json2py(value, recursive=recursive) # type: ignore

        new[key] = value

    return new

Then, you could do something like:

options = {
    "Max-Age": 0,
    "Expires": ""
}

set_cookie("cookieName", "cookieValue", **json2py(options)) # Will not account for PEP-8 upper/lowercasing (e.g. max_age, expires)

Sadly though, some typing-information will be lost (although you could use dataclass like objects to get them back).

Since TypedDict is both one of the most useful and most special cased typing constructs, I’m never too worried about # type: ignore ing stuff that strips the hints. The usefulness of TypedDict exists only at lint time, so if I know that something transforms the dictionary in a compliant way I’ll just force it.

from typing import Any, TypedDict, assert_type

class Opts(TypedDict):
    max_age: int
    expires: str

def json2py[M](val: dict[str, Any], td: type[M] = dict, *, recursive: bool = True) -> M:
    return {
       key.replace('-', '_').lower(): json2py(val) if recursive else val
       for key, val in val.items()
    } if isinstance(val, dict) else val # type: ignore

converted = json2py({'Max-Age': 0, 'Expires': ''}, Opts)
assert_type(converted, Opts)

That one’s an absolute mess, but you can tuck it away deep in your private API so types can still be handled correctly in your public API.