# 2.4. Star Arguments¶

• Unpack and Arbitrary Number of Parameters and Arguments

## 2.4.1. Recap¶

• Argument - Value or variable being passed to the function

• Positional arguments - value passed to function

• Positional arguments - order is important

• Positional arguments - must be at the left side

• Keyword arguments - value passed to function resolved by name

• Keyword arguments - order is not important

• Keyword arguments - must be on the right side

• Positional argument cannot follow keyword arguments

>>> def echo(a=None, b=None):
...     ...

>>> echo(1)          # positional argument
>>> echo(a=1)        # keyword argument
>>> echo(1, 2)       # positional arguments
>>> echo(2, 1)       # positional arguments
>>> echo(a=1, b=2)   # keyword arguments
>>> echo(b=2, a=1)   # keyword arguments, order doesn't matter
>>> echo(1, b=2)     # positional and keyword arguments

>>> echo(a=1, 2)
Traceback (most recent call last):
SyntaxError: positional argument follows keyword argument


## 2.4.2. Positional Arguments¶

• * is used for positional arguments

• there is no convention, but you can use any name

• * unpacks from tuple, list or set

>>> def echo(a, b, c):
...     print(f'{a=}, {b=}, {c=}')

>>> echo(1, 2, 3)
a=1, b=2, c=3

>>> data = (1, 2, 3)
>>>
>>> echo(data[0], data[1], data[2])
a=1, b=2, c=3
>>>
>>> echo(*data)
a=1, b=2, c=3

>>> data = (1, 2, 3)
>>>
>>> echo(data)
Traceback (most recent call last):
TypeError: echo() missing 2 required positional arguments: 'b' and 'c'


## 2.4.3. Keyword Arguments¶

• ** is used for keyword arguments

• there is no convention, but you can use any name

• ** unpacks from dict

Keyword arguments passed directly:

>>> def echo(a, b, c):
...     print(f'{a=}, {b=}, {c=}')

>>> echo(a=1, b=2, c=3)
a=1, b=2, c=3

>>> data = {'a': 1, 'b': 2, 'c': 3}
>>>
>>> echo(a=data['a'], b=data['b'], c=data['c'])
a=1, b=2, c=3
>>>
>>> echo(**data)
a=1, b=2, c=3
>>>
>>> echo(*data.values())
a=1, b=2, c=3

>>> data = {'a': 1, 'b': 2, 'c': 3}
>>>
>>> echo(data)
Traceback (most recent call last):
TypeError: echo() missing 2 required positional arguments: 'b' and 'c'


## 2.4.4. Positional and Keyword Arguments¶

>>> def echo(a, b, c, d):
...     print(f'{a=}, {b=}, {c=}, {d=}')

>>> echo(1, 2, c=3, d=4)
a=1, b=2, c=3, d=4

>>> data1 = (1, 2)
>>> data2 = {'c': 3, 'd': 4}
>>>
>>> echo(data1[0], data1[1], c=data2['c'], d=data2['d'])
a=1, b=2, c=3, d=4
>>>
>>> echo(*data1, **data2)
a=1, b=2, c=3, d=4

>>> data1 = (1, 2)
>>> data2 = {'c': 3}
>>>
>>> echo(*data1, **data2)
Traceback (most recent call last):
TypeError: echo() missing 1 required positional argument: 'd'

>>> data1 = (1, 2)
>>> data2 = {'c': 3, 'd': 4, 'a': 1}
>>>
>>> echo(*data1, **data2)
Traceback (most recent call last):
TypeError: echo() got multiple values for argument 'a'


## 2.4.5. Create One Object¶

>>> class Iris:
...     def __init__(self, sepal_length, sepal_width, petal_length, petal_width, species):
...         self.sepal_length = sepal_length
...         self.sepal_width = sepal_width
...         self.petal_length = petal_length
...         self.petal_width = petal_width
...         self.species = species
...
...     def __repr__(self):
...         values = tuple(vars(self).values())
...         return f'Iris{values}'


Objects From Sequence:

>>> DATA = (5.8, 2.7, 5.1, 1.9, 'virginica')
>>>
>>> result = Iris(*DATA)
>>> print(result)
Iris(5.8, 2.7, 5.1, 1.9, 'virginica')


Objects From Mappings:

>>> DATA = {
...     'sepal_length': 5.8,
...     'sepal_width': 2.7,
...     'petal_length': 5.1,
...     'petal_width': 1.9,
...     'species': 'virginica',
... }
>>>
>>> result = Iris(**DATA)
>>> print(result)
Iris(5.8, 2.7, 5.1, 1.9, 'virginica')


## 2.4.6. Create Many Objects¶

>>> from pprint import pprint
>>>
>>>
>>> class Iris:
...     def __init__(self, sepal_length, sepal_width, petal_length, petal_width, species):
...         self.sepal_length = sepal_length
...         self.sepal_width = sepal_width
...         self.petal_length = petal_length
...         self.petal_width = petal_width
...         self.species = species
...
...     def __repr__(self):
...         values = tuple(vars(self).values())
...         return f'Iris{values}'


Object from list of sequences:

>>> 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]
>>>
>>> pprint(result)
[Iris(5.8, 2.7, 5.1, 1.9, 'virginica'),
Iris(5.1, 3.5, 1.4, 0.2, 'setosa'),
Iris(5.7, 2.8, 4.1, 1.3, 'versicolor'),
Iris(6.3, 2.9, 5.6, 1.8, 'virginica'),
Iris(6.4, 3.2, 4.5, 1.5, 'versicolor'),
Iris(4.7, 3.2, 1.3, 0.2, 'setosa')]


Objects from list of mappings:

>>> 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]
>>>
>>> pprint(result)
[Iris(5.8, 2.7, 5.1, 1.9, 'virginica'),
Iris(5.1, 3.5, 1.4, 0.2, 'setosa'),
Iris(5.7, 2.8, 4.1, 1.3, 'versicolor'),
Iris(6.3, 2.9, 5.6, 1.8, 'virginica'),
Iris(6.4, 3.2, 4.5, 1.5, 'versicolor'),
Iris(4.7, 3.2, 1.3, 0.2, 'setosa')]


Objects from json:

>>> DATA = (
...     '[{"sepal_length":5.8,"sepal_width":2.7,"petal_length":5.1,"peta'
...     'l_width":1.9,"species":"virginica"},{"sepal_length":5.1,"sepal_'
...     'width":3.5,"petal_length":1.4,"petal_width":0.2,"species":"seto'
...     'sa"},{"sepal_length":5.7,"sepal_width":2.8,"petal_length":4.1,"'
...     'petal_width":1.3,"species":"versicolor"},{"sepal_length":6.3,"s'
...     'epal_width":2.9,"petal_length":5.6,"petal_width":1.8,"species":'
...     '"virginica"},{"sepal_length":6.4,"sepal_width":3.2,"petal_lengt'
...     'h":4.5,"petal_width":1.5,"species":"versicolor"},{"sepal_length'
...     '":4.7,"sepal_width":3.2,"petal_length":1.3,"petal_width":0.2,"s'
...     'pecies":"setosa"}]'
... )
>>>
>>> import json
>>> result = [Iris(**row) for row in json.loads(DATA)]
>>>
>>> pprint(result)
[Iris(5.8, 2.7, 5.1, 1.9, 'virginica'),
Iris(5.1, 3.5, 1.4, 0.2, 'setosa'),
Iris(5.7, 2.8, 4.1, 1.3, 'versicolor'),
Iris(6.3, 2.9, 5.6, 1.8, 'virginica'),
Iris(6.4, 3.2, 4.5, 1.5, 'versicolor'),
Iris(4.7, 3.2, 1.3, 0.2, 'setosa')]


## 2.4.7. Recap¶

>>> 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'),
... ]
>>>
>>>
>>> def show(sl, sw, pl, pw, species):
...     avg = sum([sl,sw,pl,pw]) / len([sl,sw,pl,pw])
...     return species, avg, sl, sw, pl, pw
>>>
>>>
>>> for *values, species in DATA:
...     name, avg, *data = show(*values, species)
...     print(f'{avg=:.1f}, {values=}, {species=}')
avg=3.9, values=[5.8, 2.7, 5.1, 1.9], species='virginica'
avg=2.5, values=[5.1, 3.5, 1.4, 0.2], species='setosa'
avg=3.5, values=[5.7, 2.8, 4.1, 1.3], species='versicolor'
avg=4.1, values=[6.3, 2.9, 5.6, 1.8], species='virginica'
avg=3.9, values=[6.4, 3.2, 4.5, 1.5], species='versicolor'
avg=2.4, values=[4.7, 3.2, 1.3, 0.2], species='setosa'


## 2.4.8. Use Case - 0x01¶

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)


## 2.4.9. Use Case - 0x02¶

>>> 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


## 2.4.10. Use Case - 0x03¶

>>> 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)


## 2.4.11. Use Case - 0x04¶

>>> 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)]


## 2.4.12. Use Case - 0x04¶

>>> 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')]


## 2.4.13. Use Case - 0x05¶

>>> 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')]


## 2.4.14. Use Case - 0x06¶

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']}


## 2.4.15. Use Case - 0x07¶

• 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('myfile.csv', ';', None, 'infer', None, None, None, False, None,
...          True, None, None, None, None, None, False, 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('myfile1.csv')
>>> mycsv('myfile2.csv')
>>> mycsv('myfile3.csv')
>>> mycsv('myfile4.csv')
>>> mycsv('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('myfile.csv', encoding='utf-8')
>>> mycsv('myfile.csv', encoding='utf-8', verbose=True)
>>> mycsv('myfile.csv', verbose=True, usecols=['sepal_length', 'species'])


## 2.4.16. Use Case - 0x08¶

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'


## 2.4.18. Assignments¶

"""
* Assignment: Star Arguments Define
* Complexity: easy
* Lines of code: 3 lines
* Time: 8 min

English:
1. Define result: list[dict]
2. Iterate over DATA separating values from species
3. To result append dict with:
a. key: species, value: species name
b. 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:
a. klucz: species, wartość: nazwa gatunku
b. klucz: mean, wartość: wynik średniej arytmetycznej values
4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
>>> import sys; sys.tracebacklimit = 0

>>> 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 = ...


"""
* Assignment: Star Arguments Range
* Complexity: medium
* Lines of code: 25 lines
* Time: 13 min

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ść

Hint:
* https://github.com/python/cpython/blob/main/Objects/rangeobject.c#LC75
* raise TypeError('error message')
* if len(args) == ...

Tests:
>>> import sys; sys.tracebacklimit = 0
>>> 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):
current = start
result = []

while current < stop:
result.append(current)
current += step

return result