Strict Python function parameters
sethmlarson.dev
sethmlarson.dev
For example, this doesn't do what people expect:
def foo(bar=Bar())
...
Because this creates exactly one Bar at function definition time, not a Bar per function call.Instead what people usually want is:
def foo(bar=None)
if bar is None:
bar = Bar()
...
I hate having to have a block of `if bar is None` at the top of my functions.I wonder if there's a library that helps make creating functions with default args easier? Or any tips people have?
Anyways, my point is that I can write `if X is None: X = bla` and be confident it will be correct in almost all situations. For `X = X or Y` there are some nasty corner cases where the distinction between `[]` and `""` and `{}` is somehow important, or the distinction between `is` and `==` is important, or different falsy values have different annotations attached to them, or or or....
Which is why I just do `if param is None:... ` and not worry about the few extra lines of code and move on.
The trick does have the nice property of delaying execution for evaluating what the default parameter though. The only other way to do that would involve writing lambdas which is even more code? The sibling comment suggesting a package looks like even more code to write as a user. I think any really elegant fix would have to be done in the language itself.
To me it's like complaining that (++a) is a "trick" that lets you use secret temp values that are hard to understand. I mean.. _I guess_, but it's C so it seems like the problem is that you don't like C.
I'd also like whatever a solution is to work well with type annotations.
I'm surprised there isn't a pep to improve the situation. (Maybe there is and I haven't found it!)
If you make the functions to supply late-bound defaults arguments to the decorator, its pretty doable; here’s what I just threw together.
from inspect import signature, Parameter
def with_deferred_defaults(*args, **kwargs):
def decorate(fun):
parameters = signature(fun).parameters
positional_parameters = [parameter for parameter in parameters.values() if parameter.kind in [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD]]
defaults = list(args)
kwdefaults = kwargs
def wrapper(*args, **kwargs):
args = list(args)
if len(args) < len(defaults):
args = args + [arg() for arg in defaults[len(args):]]
new_kwargs = {k: v() for k, v in kwdefaults.items()}
new_kwargs.update(kwargs)
for i, p in enumerate(positional_parameters):
if i >= len(args):
if p.name in new_kwargs:
args.extend([None]*(1+i-len(args)))
args[i] = new_kwargs[p.name]
del new_kwargs[p.name]
elif p.default is Parameter.empty:
raise TypeError('Missing argument: {}'.format(p.name))
else:
if p.name in new_kwargs:
del new_kwargs[p.name]
return fun(*args, **new_kwargs)
return wrapper
return decorate def foo(Bar=Bar):
bar = Bar()
...
Pass in the class or any callable and then call it in the body to make an instance.This is commonly used to implement logging callbacks e.g.
from collections.abc import Callable
def log_fn(message: str = ''):
print(f'This is your log: {message}')
def run_read_file(file_to_read:str, log_handler: Callable=log_fn):
contents = open(file_to_read).readlines()
for line in contents:
log_handler(message=line)In general I just try my best to avoid immutable, or dynamic defaults, but sometimes they're necessary and I make that same if block at the top of the function. If I catch myself copy/pasting the same setup to multiple functions, then it might be time for a class or a decorator.
def fn(s, n=>len(s)):
"=>" just kind of bugs me, for some reason. (I love me some bikeshedding.)Of the alternatives they offer, ":=" kind of looks nicest to me:
def fn(s, n:=len(s)):
Though it doesn't really line up with the semantics of the existing walrus operator. Also, as the PEP mentions, it could look confusing when mixed with type annotations.Semantically, the "?=" alternative might be best, since it kind of makes it clear it's a default, and doesn't conflict with anything else in the language:
def fn(s, n?=len(s)):
Kind of reads like "present? else = ...".edit: At this point I'd shift from "might be best" to "almost certainly is best". I'm now a zealous "?=" advocate.
`x ?= default` is perfect IMHO. There's already prior art (Makefiles) that this means exactly "present? else = ..." with lazy evaluation of RHS expression.
It also would allow eliminating the "if x is SENTINEL" pattern, where None is a valid non-default argument.
The only potential downside is that standard, function definition-time default arguments also imply "present? else = ...". But I think of all the options available, this one makes the most sense.
def foo(..., cache={}):
...
to implement cached functions, or functions which need an internal cache for whatever reason. Later versions of Python provide an lru cache decorator, but that doesn't help with the internal cache and can be inefficient when you don't want your cache to forget any results.I'm not sure it handles the use case op is talking about.
IE def foo(bar=list()): # this creates a mutable list shard by all calls of the function. Very unlikely something you want.
Maybe the pep would allow for def foo(bar=>list()):
But the pep does not address this use case at all
>Function parameters can have default values which are calculated during function definition and saved. This proposal introduces a new form of argument default, defined by an expression to be evaluated at function call time.
That said, it is not always desirable that a new object is created each time the function is called without the arg. One obvious example is when the default is an immutable object that's either large or expensive to construct.
Doing it the if-statement way is the clearest and easiest for other programmers to follow, therefore it is the best way.
Or an empty list.
Or any other falsish value.
https://docs.python.org/3/library/dataclasses.html#dataclass...
Functions are in fact objects as well, so there might be some way to do this sort of behavior with decorators.
I would actually contend that "if x is None, x =Bar()" is actually a superior pattern in some/many cases. I used to use "x or Bar()" or "x if x is not None else Bar()" quite a lot, due to its compactness. However I learned that actually sometimes code coverage fails to ensure proper coverage of these ternary assignments. If your default argument is just because you need to initialize a mutable object, ternary works fine. However if the presence or lack of a parameter is an important part of your state space, you probably want explicit coverage.
https://stackoverflow.com/questions/47958166/code-coverage-a...
Both in terms of enhancing one's understanding of python's memory model, and in terms of it being an incredibly useful feature, e.g. for introducing static/persistent variables to a function (without the need for global declarations)
I would not want to see this "fixed"
(let ((my-list '(1 2 3)))
(defun foo (&optional (l my-list))
(setf (car l) 'hello)
l))
or, more often, just make it a special variable of some sort: (defvar *my-list* '(1 2 3))
(defun foo (&optional (l *my-list*))
(setf (car l) 'hello)
l))
This seems to work fine, and brings attention to "I have an object that supports mutation, and I'm actually going to mutate it," which is an unusual enough case to be worth calling out. def myfun(person = Person()): ...
will evaluate `Person()` only once. If you want it to be evaluated every time, you have to say: def myfun(person = None):
if person is None: person = Person()
...
What's being suggested, is to change the language so that the default-arg code: def myfun(person = Person()): ...
will evaluate `Person()` on every call. If you wanted it evaluated only once, you would have to say: __default_person = Person()
def myfun(person = __default_person): ...
In either case, it's pretty easy to obtain the other case, and the question is what the best default is. My impression is that "evaluate every time" is both a lot less surprising and more commonly what you want, so it would be the better default.It makes sense if you realize that the function is defined once, and for the function need to be defined, its signature needs to be defined with it. Default arguments are part of the signature, not part of the function body.
It would make sense to pass a callable without calling it, like
def myfun(person = Person): ...
but if the call occurs in the function signature, it doesn't really make sense that the call would somehow happen again and again. You're not defining a function in the signature; you're specifying a value right there.The current PEP proposal is to enable the "evaluate at function call time" ability with new syntax, which I think makes the most sense semantically and also doesn't break existing code.
There are things besides functions and values! They're called "expressions" or "code blocks", and in an impure language like Python they can produce a different value each time you evaluate them. In my second example, "Person()" is neither a value nor a function -- it's an expression that would be evaluated each time the function is run. This isn't that weird; in `while condition: body`, `body` is a block that's evaluated multiple times. It would need to capture variables from the local environment though, which makes it more complicated to implement.
> The current PEP proposal is to enable the "evaluate at function call time" ability with new syntax, which I think makes the most sense semantically and also doesn't break existing code.
Oh yeah I wasn't suggesting breaking existing code. Just that if I was designing a language from scratch, it would probably avoid the Python foot-gun and have default arguments evaluated each time.
> Both in terms of enhancing one's understanding of python's memory model...
In much the same way as having your company bricked by a buffer-overflow ransomware attack does in the C world? I'm not a big fan of elucidation by gotcha.
> [...and] for introducing static/persistent variables to a function (without the need for global declarations)
Except when you are at risk of someone calling with their own variable(s) that do not have the static-singleton semantics that the default would have.
When performing Python interviews I usually try to have that behavior come up in the coding question - not to quiz for trivia so much as to get a feeling for adding on battle-scar bonus points.
In practice, for any non plain-ol-data defaults I just have it default to None and handle it in logic.
Totally understand why it may be useful for introducing a persistent store to a function, but as a matter of taste I like to do that in an explicit decorator if needed.
from functools import wraps
def defaults(**default_kwargs): # Decorator factory
def decorator(func): # Decorator function
@wraps(func)
def wrapper(*args, **kwargs): # Actual wrapper
# Iterate over all default keyword arguments
for name in default_kwargs.keys():
# And if not specified in the passed in kwargs use the default
kwargs[name] = kwargs.get(name, default_kwargs[name]())
# Call the actual function
return func(*args, **kwargs)
return wrapper
return decorator
# Note that the default argument value should be a callable to avoid the pitfall with mutable arguments
@defaults(bar=lambda: [1, 2, 3])
def foo(bar):
print(bar)
foo()https://gist.github.com/softwaredoug/bbfa2541d48c685908ebd6a...
def foo(bar: list):
print(bar)
that type checking will be happy.Why? This is like someone saying "I don't like declaring my variables in C at the top of the function".
It is a good and readable solution to a somewhat rare problem. Passing mutable objects as default function parameters is a common code smell in Python.
And no, adding a special syntax to an already syntax-heavy language to address this is not good.
It’s boilerplate that distracts from the purpose of the function, this getting in the way of readability.
It’s also just more code prone to a programmer screwing up from time to time, despite how simple it is.
Functions are not a good tool for storing stateful objects in Python. That's why classes exist.
def foo(bar=None):
bar = bar or Bar()
if you know that bar can't evaluate to false in edge cases.What is your list of "these little hang ups you only know about once you’ve pulled your hair out" if you don't mind sharing?
Apart from the common solutions (initialize to None and check, etc.) this is exactly what this PEP is about: https://www.python.org/dev/peps/pep-0671/
Personally, I find * useful and / fugly - because positional parameters are very opaque when reading code. They also don't give a chance to the IDE to guess what each parameter is meant for.
They are useful, but I think sometimes the advanced features run into the basic interfaces a bit, making it weirdly tricky to teach beginners without being confused.
https://pydantic-docs.helpmanual.io/
https://github.com/tiangolo/typer
We're using Pydantic for Robusta (https://github.com/robusta-dev/robusta) and absolutely love it. You get the best of traditional Python (rapid prototyping and no boilerplate) while still being able to scale your codebase and keep it maintainable. Robusta is the first large project I've written in Python where I'm not encountering type errors at runtime left and right.
One thing to note about it is tiangolo is pretty content to maintain singular control over his projects which means the project only really advances when he has the time or interest to work on that specific project.
Yes, he recently responded to some criticism of this basically saying that he has no real intention to increase the bus factor or permit more community control to his projects, which is obviously his right, but is also something that people should consider when deciding whether or not to go with them.
Type hints are great when combined with a good IDE such as Pycharm, but are otherwise useless for Python. Sometimes, they are even missleading. Often they don't cover all the call options.
Complicated function calls should be made with keywords anyway. Leave positional only parameters to obvious functions such as pow() or sqrt().
The speed hit should be minimal. You are using Python, where dot access is a much more bigger performance eater than keyword arg functions.
Python does not have the scale to make typing worthwhile, but people are already making it a required Python skill.
I this feature is optional, then I should not be required to know it, right?
If you want static typing, please use another language that makes use of them to actually speed up the code. Let's keep the docstrings for documentation.
When I mentioned scaling I had in mind other issues with a large codebase - not actual performance at runtime.
For example, before type hints I found it difficult (even with PyCharm and other IDEs) to safely refactor code. I would end up missing specific usages of an old member name and discover it at runtime when the code raised an exception. In the best case scenario I would find it when running tests. In the worst case, in production.
Type hints make this a moot point, at least in real world usage. With type hints the IDE is smart enough to find all references when refactoring.
I am currently finishing up a moderately sized project in FastAPI, and for my next project I will strongly be recommending using Starlette alone.
[0] https://chrome.google.com/webstore/detail/anki-quick-adder/g...
def foo(obvious_thing:type, *, modifier:type, modifier2:type, …)
Because I find this call incomprehensible: foo(items, 12, “apple”)
and this immediately better: foo(items, num=12, kind=“apple”)
and preventing my callers from being able to do the first is a huge win.foo(items, num=12, kind=“apple”)
vs:
foo(items, num=num, kind=kind)
?
Then you might as well get your info from the variable names in the calling method.
foo(items, num=, kind=)
# equivalent to
foo(items, num=num, kind=kind)
But as far as I know there hasn't been a formal proposal (I could be wrong, and am happy to be corrected on this). x=5
print(f”{x=}”) foo(items, kind, num)
and then everyone is sad again.They indicate that you are indeed passing the required arguments. So, the
def foo(val, querry=querry):
...
doesn't look as funny as you might think.you call it “power play”, i call it “having an API for my libraries”; potato, potato.
def verb(object, **adjectives)
is the pythonic way to pass an “options” dict. It doesn’t make sense as positional parameters and makes for a worse API. process_data(**kwargs)
and have the data field pulled out of kwargs. Of course you can still do data = kwargs.pop(“data”)
process_data(data, **kwargs)
but if you are the only library doing it, it can cause things to break when it breaks programmers expectations.Just call it as `*args, **kwargs`. That's something you need to do when forwarding anyway.
You can't always trust documentation to be accurate and up to date.
I have an article in the earlier stages about what I call the "keyword arguments blender" and how to avoid it, I think it's a common anti-pattern in lots of Python code (including my own, guilty!)
I can see the benefit of forcing junior members of my team to use keyword arguments, but I would be so annoyed if a library author did it to me.
Unless there is a technical reason, your callables should just work the same way as all the others.
I can't think of any reason to use positional only arguments, but keyword only arguments would be nice if I had a variable length of positional arguments that get packed, but then wanted to have an optional argument to change some behavior. But even then it's normally better to just have the caller send an iterable.
The article uses as an example of two arguments, a byte string and a string specifying the character encoding of the first argument. Having two arguments so strongly associated with each other as separate arguments is a slight code smell. One should contemplate unifying them into a compound type (like a class instance) which includes both arguments. It’s not always reasonable, of course, and may add more complexity than it saves. But one should keep an eye out for this.
If you need something to store 7-8 different value types, either make a class or reach for named tuple.
I tried using regular tuples as lightweight objects, it does not work.
I do think this moves the problem of possible argument confusion to the construction of the new type. At some point the association must be expressed in code, and if the association is between two things of the same type the type system can't flag mistakes as errors. A simple data class still helps, even if it doesn't eliminate the problem, because people can more easily remember one Point(x, y) constructor than where "x" and "y" appear in dozens of different functions.
There are rare cases that actually call for keyword-only and (even more rarely) positional-only arguments, but unless they are required for your usecase, please do not enforce arbitrary argument types on your users.
This frees up "strict".
void foo(int? num, {String? text}, [List<int> nums]){}