3.4. Star Arguments
Unpack and Arbitrary Number of Parameters and Arguments
*
is used for positional arguments**
is used for keyword argumentsecho(*data)
- unpacks from sequence (tuple
,list
,set
, etc)echo(**data)
- unpacks from mapping (dict
, etc)echo(*data1, **data2)
- unpacks from sequence and mappings
3.4.1. Recap
Argument - value passed to the function
Argument can be: positional or keyword
Positional arguments - resolved by position, order is important, must be at the left side
Keyword arguments - resolved by name, order is not important, must be on the right side
After first keyword argument, all following arguments must also be keyword
>>> def echo(a, b):
... ...
Positional arguments (order is important):
>>> echo(1, 2)
>>> echo(2, 1)
Keyword arguments (order is not important):
>>> echo(a=1, b=2)
>>> echo(b=2, a=1)
Positional and keyword arguments:
>>> echo(1, b=2)
Positional arguments must be at the left side:
>>> echo(a=1, 2)
Traceback (most recent call last):
SyntaxError: positional argument follows keyword argument
3.4.2. Positional Arguments
echo(*data)
- unpacks from sequence (tuple
,list
,set
, etc)*
is used for positional argumentsThere is no convention, so you can use any name, for example
*data
>>> def echo(a, b, c, d):
... print(f'{a=}, {b=}, {c=}, {d=}')
...
>>> data = (1, 2, 3, 4)
Without star unpacking:
>>> echo(data[0], data[1], data[2], data[3])
a=1, b=2, c=3, d=4
With start unpacking:
>>> echo(*data)
a=1, b=2, c=3, d=4
3.4.3. Keyword Arguments
echo(**data)
- unpacks from mapping (dict
, etc)**
is used for keyword argumentsThere is no convention, so you can use any name, for example
**data
Keyword arguments passed directly:
>>> def echo(a, b, c, d):
... print(f'{a=}, {b=}, {c=}, {d=}')
...
>>> data = {'a':1, 'b':2, 'c':3, 'd':4}
Without star unpacking:
>>> echo(a=data['a'], b=data['b'], c=data['c'], d=data['d'])
a=1, b=2, c=3, d=4
With start unpacking:
>>> echo(**data)
a=1, b=2, c=3, d=4
3.4.4. Positional and Keyword Arguments
echo(*data1, **data2)
- unpacks from sequence and mappings*
is used for positional arguments**
is used for keyword argumentsThere is no convention, so you can use any name, for example
*data1
There is no convention, so you can use any name, for example
**data2
>>> def echo(a, b, c, d):
... print(f'{a=}, {b=}, {c=}, {d=}')
...
>>> data1 = (1, 2)
>>> data2 = {'c':3, 'd':4}
Without star unpacking:
>>> echo(data1[0], data1[1], c=data2['c'], d=data2['d'])
a=1, b=2, c=3, d=4
With star unpacking:
>>> echo(*data1, **data2)
a=1, b=2, c=3, d=4
3.4.5. Merge Kwargs
echo(**data1, **data2)
>>> def echo(a, b, c, d):
... return locals()
>>>
>>> data1 = {'a':1, 'b':2}
>>> data2 = {'c':3, 'd':4}
With star unpacking:
>>> echo(**data1, **data2)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
3.4.6. Merge Dicts
dict(**data1, **data2)
- old way{**data1, **data2}
- old waydata1 | data2
- since Python 3.9 preferred way
>>> data1 = {'a':1, 'b':2}
>>> data2 = {'c':3, 'd':4}
Before Python 3.9 merging dicts was done with dict
or {}
:
>>> dict(**data1, **data2)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>> {**data1, **data2}
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
Since Python 3.9 there is a dedicated operator for merging dicts:
>>> data1 | data2
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
3.4.7. Create Objects
One object from sequence
One object from mapping
Many objects from sequence of sequences
Many objects from sequence of mappings
>>> class User:
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
...
... def __repr__(self):
... return f"User(firstname='{self.firstname}', lastname='{self.lastname}')"
One object from sequence:
>>> data = ('Mark', 'Watney')
>>>
>>> result = User(*data)
>>> result
User(firstname='Mark', lastname='Watney')
One object from mapping:
>>> data = {'firstname': 'Mark', 'lastname': 'Watney'}
>>>
>>> result = User(**data)
>>> result
User(firstname='Mark', lastname='Watney')
Many objects from sequence of sequences:
>>> data = [
... ('Mark', 'Watney'),
... ('Melissa', 'Lewis'),
... ('Rick', 'Martinez'),
... ]
>>>
>>> result = [User(*row) for row in data]
>>> result
[User(firstname='Mark', lastname='Watney'),
User(firstname='Melissa', lastname='Lewis'),
User(firstname='Rick', lastname='Martinez')]
Many objects from sequence of mappings:
>>> data = [
... {'firstname': 'Mark', 'lastname': 'Watney'},
... {'firstname': 'Melissa', 'lastname': 'Lewis'},
... {'firstname': 'Rick', 'lastname': 'Martinez'},
... ]
>>>
>>> result = [User(**row) for row in data]
>>> result
[User(firstname='Mark', lastname='Watney'),
User(firstname='Melissa', lastname='Lewis'),
User(firstname='Rick', lastname='Martinez')]
3.4.8. Recap
*
is used for positional arguments**
is used for keyword argumentsecho(*data)
- unpacks from sequence (tuple
,list
,set
, etc)echo(**data)
- unpacks from mapping (dict
, etc)echo(*data1, **data2)
- unpacks from sequence and mappingsCreate one object from sequence
Create one object from mapping
Create many objects from sequence of sequences
Create many objects from sequence of mappings
Old way to merge dicts
dict(**data1, **data2)
or{**data1, **data2}
Since Python 3.9 merge dicts with:
data1 | data2
3.4.9. Use Case - 1
Calling a function which has similar parameters. Passing configuration to the function, which sets parameters from the config:
>>> def draw_line(x, y, color, type, width, markers):
... ...
>>> draw_line(x=1, y=2, color='red', type='dashed', width='2px', markers='disc')
>>> draw_line(x=3, y=4, color='red', type='dashed', width='2px', markers='disc')
>>> draw_line(x=5, y=6, color='red', type='dashed', width='2px', markers='disc')
>>> style = {'color': 'red',
... 'type': 'dashed',
... 'width': '2px',
... 'markers': 'disc'}
>>>
>>> draw_line(x=1, y=2, **style)
>>> draw_line(x=3, y=4, **style)
>>> draw_line(x=5, y=6, **style)
3.4.10. Use Case - 2
>>> def print_coordinates(x, y, z):
... print(f'{x=}, {y=}, {z=}')
Passing sequence to the function:
>>> point = [1, 2, 3]
>>>
>>> print_coordinates(point[0], point[1], point[2])
x=1, y=2, z=3
>>>
>>> print_coordinates(*point)
x=1, y=2, z=3
Passing mapping to the function:
>>> point = {'x': 1, 'y': 2, 'z': 3}
>>>
>>> print_coordinates(x=point['x'], y=point['y'], z=point['z'])
x=1, y=2, z=3
>>>
>>> print_coordinates(**point)
x=1, y=2, z=3
>>>
>>> print_coordinates(*point.values())
x=1, y=2, z=3
Passing sequence and mapping to the function:
>>> point2d = (1, 2)
>>> point3d = {'z': 3}
>>>
>>> print_coordinates(*point2d, **point3d)
x=1, y=2, z=3
3.4.11. Use Case - 3
>>> def database_connect(host, port, username, password, database):
... ...
After reading config from file we have a dict:
>>> CONFIG = {
... 'host': 'example.com',
... 'port': 5432,
... 'username': 'myusername',
... 'password': 'mypassword',
... 'database': 'mydatabase'}
Database connection configuration read from config file:
>>> connection = database_connect(
... host=CONFIG['host'],
... port=CONFIG['port'],
... username=CONFIG['username'],
... password=CONFIG['password'],
... database=CONFIG['database'])
Or:
>>> connection = database_connect(**CONFIG)
3.4.12. Use Case - 4
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Point:
... x: int
... y: int
... z: int = 0
>>>
>>>
>>> MOVEMENT = [(0, 0),
... (1, 0),
... (2, 1, 1),
... (3, 2),
... (3, 3, -1),
... (2, 3),
... ]
>>> movement = [Point(x,y) for x,y in MOVEMENT]
Traceback (most recent call last):
ValueError: too many values to unpack (expected 2)
>>> movement = [Point(*coordinates) for coordinates in MOVEMENT]
>>>
>>> movement
[Point(x=0, y=0, z=0),
Point(x=1, y=0, z=0),
Point(x=2, y=1, z=1),
Point(x=3, y=2, z=0),
Point(x=3, y=3, z=-1),
Point(x=2, y=3, z=0)]
3.4.13. Use Case - 4
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Iris:
... sepal_length: float
... sepal_width: float
... petal_length: float
... petal_width: float
... species: str
>>>
>>>
>>> DATA = [
... (5.8, 2.7, 5.1, 1.9, 'virginica'),
... (5.1, 3.5, 1.4, 0.2, 'setosa'),
... (5.7, 2.8, 4.1, 1.3, 'versicolor'),
... (6.3, 2.9, 5.6, 1.8, 'virginica'),
... (6.4, 3.2, 4.5, 1.5, 'versicolor'),
... (4.7, 3.2, 1.3, 0.2, 'setosa'),
... ]
>>>
>>>
>>> result = [Iris(*row) for row in DATA]
>>>
>>> print(result)
[Iris(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9, species='virginica'),
Iris(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2, species='setosa'),
Iris(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3, species='versicolor'),
Iris(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8, species='virginica'),
Iris(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5, species='versicolor'),
Iris(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2, species='setosa')]
3.4.14. Use Case - 5
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Iris:
... sepal_length: float
... sepal_width: float
... petal_length: float
... petal_width: float
... species: str
>>>
>>>
>>> DATA = [
... {"sepal_length":5.8,"sepal_width":2.7,"petal_length":5.1,"petal_width":1.9,"species":"virginica"},
... {"sepal_length":5.1,"sepal_width":3.5,"petal_length":1.4,"petal_width":0.2,"species":"setosa"},
... {"sepal_length":5.7,"sepal_width":2.8,"petal_length":4.1,"petal_width":1.3,"species":"versicolor"},
... {"sepal_length":6.3,"sepal_width":2.9,"petal_length":5.6,"petal_width":1.8,"species":"virginica"},
... {"sepal_length":6.4,"sepal_width":3.2,"petal_length":4.5,"petal_width":1.5,"species":"versicolor"},
... {"sepal_length":4.7,"sepal_width":3.2,"petal_length":1.3,"petal_width":0.2,"species":"setosa"},
... ]
>>>
>>>
>>> result = [Iris(**row) for row in DATA]
>>>
>>> print(result)
[Iris(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9, species='virginica'),
Iris(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2, species='setosa'),
Iris(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3, species='versicolor'),
Iris(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8, species='virginica'),
Iris(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5, species='versicolor'),
Iris(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2, species='setosa')]
3.4.15. Use Case - 6
Calling function with all variables from higher order function. locals()
will return a dict
with all the variables in local scope of the function:
>>> def template(template, **user_data):
... print('Template:', template)
... print('Data:', user_data)
>>>
>>>
>>> def controller(firstname, lastname, uid=0):
... groups = ['admins', 'astronauts']
... permission = ['all', 'everywhere']
... return template('user_details.html', **locals())
>>>
>>> # template('user_details.html',
>>> # firstname='Mark',
>>> # lastname='Watney',
>>> # uid=0,
>>> # groups=['admins', 'astronauts'],
>>> # permission=['all', 'everywhere'])
>>>
>>>
>>> controller('Mark', 'Watney')
Template: user_details.html
Data: {'firstname': 'Mark',
'lastname': 'Watney',
'uid': 0,
'groups': ['admins', 'astronauts'],
'permission': ['all', 'everywhere']}
3.4.16. Use Case - 7
Definition of
pandas.read_csv()
function [1]Proxy functions. One of the most common use of
*args
,**kwargs
:
>>> def read_csv(filepath_or_buffer, /, *, sep=', ', delimiter=None,
... header='infer', names=None, index_col=None, usecols=None,
... squeeze=False, prefix=None, mangle_dupe_cols=True,
... dtype=None, engine=None, converters=None, true_values=None,
... false_values=None, skipinitialspace=False, skiprows=None,
... nrows=None, na_values=None, keep_default_na=True,
... na_filter=True, verbose=False, skip_blank_lines=True,
... parse_dates=False, infer_datetime_format=False,
... keep_date_col=False, date_parser=None, dayfirst=False,
... iterator=False, chunksize=None, compression='infer',
... thousands=None, decimal=b'.', lineterminator=None,
... quotechar='"', quoting=0, escapechar=None, comment=None,
... encoding=None, dialect=None, tupleize_cols=None,
... error_bad_lines=True, warn_bad_lines=True, skipfooter=0,
... doublequote=True, delim_whitespace=False, low_memory=True,
... memory_map=False, float_precision=None): ...
Calling function with positional only arguments is insane. In Python we don't do that, because we have keyword arguments.
>>> read_csv('/tmp/myfile.csv', ';', None, 'infer', None, None, None, False,
... True, None, None, None, None, None, False, None, None, None,
... None, True, True, False, True, False, False, False, None, False,
... False, None, 'infer', None, b',', None, '"', 0, None, None,
... None, None, None, True, True, 0, True, False, True, False, None)
Traceback (most recent call last):
TypeError: read_csv() takes 1 positional argument but 49 were given
Keyword arguments with sensible defaults are your best friends. The number of function parameters suddenly is not a problem:
>>> read_csv('myfile1.csv', delimiter=';', decimal=b',')
>>> read_csv('myfile2.csv', delimiter=';', decimal=b',')
>>> read_csv('myfile3.csv', delimiter=';', decimal=b',')
>>> read_csv('myfile4.csv', delimiter=';', decimal=b',')
>>> read_csv('myfile5.csv', delimiter=';', decimal=b',')
Proxy functions allows for changing defaults to the original function. One simply define a function which has sensible defaults and call the original function setting default values automatically:
>>> def mycsv(file, delimiter=';', decimal=b',', **kwargs):
... return read_csv(file, delimiter=delimiter, decimal=decimal, **kwargs)
Thanks to using **kwargs
there is no need to specify all the values
from the original function. The uncovered arguments will simply be put
in kwargs
dictionary and passed to the original function:
>>> mycsv('/tmp/myfile1.csv')
>>> mycsv('/tmp/myfile2.csv')
>>> mycsv('/tmp/myfile3.csv')
>>> mycsv('/tmp/myfile4.csv')
>>> mycsv('/tmp/myfile5.csv')
This allows for cleaner code. Each parameter will be passed to mycsv
function. Then it will be checked if there is a different default value
already defined. If not, then parameter will be stored in kwargs
and
passed to the original function:
>>> mycsv('/tmp/myfile.csv', encoding='utf-8')
>>> mycsv('/tmp/myfile.csv', encoding='utf-8', verbose=True)
>>> mycsv('/tmp/myfile.csv', verbose=True, usecols=['sepal_length', 'species'])
3.4.17. Use Case - 8
Decorators are functions, which get reference to the decorated function as it's argument, and has closure which gets original function arguments as positional and keyword arguments:
>>> def mydecorator(func):
... def wrapper(*args, **kwargs):
... return func(*args, **kwargs)
... return wrapper
Decorators could be used on any function, therefore we could not predict what would be the name of the parameter passed to it:
>>> @mydecorator
... def add(a, b):
... return a + b
>>> @mydecorator
... def echo(text):
... return text
Moreover it depends on a user whether he/she chooses to run function positionally, using keyword arguments or even both at the same time:
>>> add(1, 2)
3
>>> add(a=1, b=2)
3
>>> add(1, b=2)
3
>>> echo('hello')
'hello'
3.4.18. References
3.4.19. Assignments
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Star Arguments Define
# - Difficulty: easy
# - Lines: 3
# - Minutes: 8
# %% English
# 1. Define `result: list[dict]`
# 2. Iterate over `DATA` separating `values` from `species`
# 3. To `result` append dict with:
# - key: `species`, value: species name
# - key: `mean`, value: arithmetic mean of `values`
# 4. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj `result: list[dict]`
# 2. Iteruj po `DATA` separując `values` od `species`
# 3. Do `result` dodawaj dict z:
# - klucz: `species`, wartość: nazwa gatunku
# - klucz: `mean`, wartość: wynik średniej arytmetycznej `values`
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> assert type(result) is list, \
'Result must be a list'
>>> assert all(type(row) is dict for row in result), \
'All elements in result must be a dict'
>>> result # doctest: +NORMALIZE_WHITESPACE
[{'species': 'virginica', 'mean': 3.875},
{'species': 'setosa', 'mean': 2.65},
{'species': 'versicolor', 'mean': 3.475},
{'species': 'virginica', 'mean': 6.0},
{'species': 'versicolor', 'mean': 3.95},
{'species': 'setosa', 'mean': 4.7}]
"""
DATA = [
('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
(5.8, 2.7, 5.1, 1.9, 'virginica'),
(5.1, 0.2, 'setosa'),
(5.7, 2.8, 4.1, 1.3, 'versicolor'),
(6.3, 5.7, 'virginica'),
(6.4, 1.5, 'versicolor'),
(4.7, 'setosa'),
]
def mean(*args):
return sum(args) / len(args)
# calculate mean and append dict with {'species': ..., 'mean': ...}
# type: list[dict]
result = ...
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Star Arguments Range
# - Difficulty: medium
# - Lines: 25
# - Minutes: 13
# %% English
# 1. Write own implementation of a built-in function `range()`,
# example usage: `myrange(0, 10)` or `myrange(0, 10, 2)`
# 2. Note, that function does not take any keyword arguments
# 3. How to implement passing only stop argument, i.e. `myrange(10)`?
# 4. Use lenght check of `*args` and `**kwargs`
# 5. Run doctests - all must succeed
# %% Polish
# 1. Zaimplementuj własne rozwiązanie wbudowanej funkcji `range()`,
# przykład użycia: `myrange(0, 10)` lub `myrange(0, 10, 2)`
# 2. Zauważ, że funkcja nie przyjmuje żanych argumentów nazwanych (keyword)
# 3. Jak zaimplementować możliwość podawania tylko końca, tj. `myrange(10)`?
# 4. Użyj sprawdzania długości `*args` i `**kwargs`
# 5. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - https://github.com/python/cpython/blob/main/Objects/rangeobject.c#LC75
# - `raise TypeError('error message')`
# - `if len(args) == ...`
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isfunction
>>> assert isfunction(myrange)
>>> myrange(0, 10, 2)
[0, 2, 4, 6, 8]
>>> myrange(0, 5)
[0, 1, 2, 3, 4]
>>> myrange(5)
[0, 1, 2, 3, 4]
>>> myrange()
Traceback (most recent call last):
TypeError: myrange expected at least 1 argument, got 0
>>> myrange(1,2,3,4)
Traceback (most recent call last):
TypeError: myrange expected at most 3 arguments, got 4
>>> myrange(stop=2)
Traceback (most recent call last):
TypeError: myrange() takes no keyword arguments
>>> myrange(start=1, stop=2)
Traceback (most recent call last):
TypeError: myrange() takes no keyword arguments
>>> myrange(start=1, stop=2, step=2)
Traceback (most recent call last):
TypeError: myrange() takes no keyword arguments
"""
# Write own implementation of a built-in function `range()`
# example: myrange(0, 10, 2), myrange(0, 10)
# note: function does not take keyword arguments
# type: Callable[[int,int,int], list[int]]
def myrange(*args, **kwargs):
if kwargs:
raise TypeError('myrange() takes no keyword arguments')
current = start
result = []
while current < stop:
result.append(current)
current += step
return result