diff --git a/README.md b/README.md index 747235e..9d156d3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). -_1827 TILs and counting..._ +_1828 TILs and counting..._ See some of the other learning resources I work on: @@ -1083,6 +1083,7 @@ If you've learned something here, support my efforts writing daily TILs by - [Dunder Methods](python/dunder-methods.md) - [Easy Key-Value Aggregates With defaultdict](python/easy-key-value-aggregates-with-defaultdict.md) - [Enable Pyright Type Checking In Cursor](python/enable-pyright-type-checking-in-cursor.md) +- [Force Remaining Arguments To Be Named](python/force-remaining-arguments-to-be-named.md) - [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md) - [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md) - [Globally Install CLI Tool With UV](python/globally-install-cli-tool-with-uv.md) diff --git a/python/force-remaining-arguments-to-be-named.md b/python/force-remaining-arguments-to-be-named.md new file mode 100644 index 0000000..42cae07 --- /dev/null +++ b/python/force-remaining-arguments-to-be-named.md @@ -0,0 +1,61 @@ +# Force Remaining Arguments To Be Named + +When the _positional argument collector_ (`*`) is used in a function definition, +all the arguments that follow it must now be explicitly named when invoking that +function. This is regardless of whether you are collecting positional arguments +into a named tuple (e.g. `*rest`) or are using the empty/dummy version (`*`). + +A friend recently recommended I use this syntax to force some configuration +values (often booleans) to be explicitly named at call sites. + +Here is an example of this in my [`py-vmt` project](https://github.com/jbranchaud/py-vmt): + +```python +class CliContext: + def __init__(self, *, verbose: bool, repo: SessionRepository | None = None) -> None: + self.verbose: bool = verbose + self.active_session: Session | None = None + self.repo: SessionRepository = repo or JsonRepository() + self.active_session = self.repo.active_session() +``` + +Notice that right after `self` is the `*` which means that both `verbose` and +`repo` must be named arguments. + +Here is a corresponding call site: + +```python + ctx.obj = CliContext(verbose=True) +``` + +Now when I initialize `CliContext`, there is no choice for me to pass in an +unnamed argument of `True`. It has to be paired with `verbose`. Above I've +excluded `repo` because that falls back to a default value. If I were to include +it, it would have to be named. + +The same applies to this scenario where I'm collecting the positional arguments. + +```python +def build_identifier(first, *rest, delimiter="/"): + if rest is None: + return first + + return delimiter.join([first, *rest]) +``` + +```python +>>> print(build_identifier("taco")) +taco +>>> print(build_identifier("taco", "bell", "mas", delimiter="-")) +taco-bell-mas +>>> print(build_identifier("taco", "bell", "mas", "-")) +taco/bell/mas/- +``` + +Notice in the third example where I don't use a named argument for the +delimiter. It instead gets swept up in the collection of positional arguments. + +This language of _positional argument collector_ I got from [Python in a +Nutshell, 4th +Edition](https://www.oreilly.com/library/view/python-in-a/9781098113544/) (see +pg. 96).