4.7. Match Sequence

A sequence pattern looks like [a, *rest, b] and is similar to a list unpacking. An important difference is that the elements nested within it can be any kind of patterns, not just names or sequences. It matches only sequences of appropriate length, as long as all the sub-patterns also match. It makes all the bindings of its sub-patterns.

4.7.1. Problem

>>> style = ['border', 'red', 1]
>>>
>>> if isinstance(style, tuple|list) and len(style) == 3:
...     element = style[0]
...     color = style[1]
...     width = style[2]
... elif isinstance(style, tuple|list) and len(style) == 2:
...     element = style[0]
...     color = style[1]
...     width = 1
... elif isinstance(style, tuple|list) and len(style) == 1:
...     element = style[0]
...     color = 'black'
...     width = 1
... elif isinstance(style, tuple|list) and len(style) == 0:
...     raise ValueError('At least one element is required')

4.7.2. Solution

>>> style = ['border', 'red', 1]
>>>
>>> match style:
...     case [element, color, width]:
...         print(f'Coloring: {element=}, {color=}, {width=}')
...     case [element, color]:
...         print(f'Coloring: {element=}, {color=}')
...     case [element]:
...         print(f'Coloring: {element=}')
...     case []:
...         raise ValueError('At least one element is required')
...
Coloring: element='border', color='red', width=1

4.7.3. Unpacking

>>> style = ['border', 'red', 1]
>>>
>>> match style:
...     case [element, *params]:
...         print(f'Coloring: {element=}, {params=}')
...     case []:
...         raise ValueError('At least one element is required')
...
Coloring: element='border', params=['red', 1]

4.7.4. Use Case - 1

>>> point = (1,2)
>>>
>>> match point:
...     case (x,y):     print(f'Point 2d: {x=}, {y=}')
...     case (x,y,z):   print(f'Point 3d: {x=}, {y=}, {z=}')
...
Point 2d: x=1, y=2
>>> point = (1,2,3)
>>>
>>> match point:
...     case (x,y):     print(f'Point 2d: {x=}, {y=}')
...     case (x,y,z):   print(f'Point 3d: {x=}, {y=}, {z=}')
...
Point 3d: x=1, y=2, z=3

4.7.5. Use Case - 2

SetUp:

>>> def handle_get(path): ...
>>> def handle_post(path): ...
>>> def handle_put(path): ...
>>> def handle_delete(path): ...

Usage:

>>> request = ['GET', '/index.html', 'HTTP/2.0']
>>>
>>> match request:
...     case ['GET', path, 'HTTP/2.0']:     handle_get(path)
...     case ['POST', path, 'HTTP/2.0']:    handle_post(path)
...     case ['PUT', path, 'HTTP/2.0']:     handle_put(path)
...     case ['DELETE', path, 'HTTP/2.0']:  handle_delete(path)

4.7.6. Use Case - 3

SetUp:

>>> def http10_get(path): ...
>>> def http11_get(path): ...
>>> def http20_get(path): ...
>>> def http30_get(path): ...

Usage:

>>> request = ['GET', '/index.html', 'HTTP/2.0']
>>>
>>> match request:  
...     case ['GET', path, 'HTTP/1.0']:  http10_get(path)
...     case ['GET', path, 'HTTP/1.1']:  http11_get(path)
...     case ['GET', path, 'HTTP/2.0']:  http20_get(path)
...     case ['GET', path, 'HTTP/3.0']:  http30_get(path)

4.7.7. Use Case - 4

SetUp:

>>> def handle_get(path): ...
>>> def handle_post(path): ...
>>> def handle_put(path): ...
>>> def handle_delete(path): ...

Usage:

>>> request = ['GET', '/index.html', 'HTTP/2.0']
>>>
>>> match request:
...     case ['GET', path, _]:     handle_get(path)
...     case ['POST', path, _]:    handle_post(path)
...     case ['PUT', path, _]:     handle_put(path)
...     case ['DELETE', path, _]:  handle_delete(path)

4.7.8. Use Case - 5

>>> request = 'GET /index.html HTTP/2.0'
>>>
>>> match request.split():  
...     case ['GET', uri, 'HTTP/1.0']:     http10.get(uri)
...     case ['GET', uri, 'HTTP/1.1']:     http11.get(uri)
...     case ['GET', uri, 'HTTP/2.0']:     http20.get(uri)
...     case ['GET', uri, 'HTTP/3.0']:     http30.get(uri)
...
...     case ['POST', uri, 'HTTP/1.0']:    http10.post(uri)
...     case ['POST', uri, 'HTTP/1.1']:    http11.post(uri)
...     case ['POST', uri, 'HTTP/2.0']:    http20.post(uri)
...     case ['POST', uri, 'HTTP/3.0']:    http30.post(uri)
...
...     case ['PUT', uri, 'HTTP/1.0']:     http10.put(uri)
...     case ['PUT', uri, 'HTTP/1.1']:     http11.put(uri)
...     case ['PUT', uri, 'HTTP/2.0']:     http20.put(uri)
...     case ['PUT', uri, 'HTTP/3.0']:     http30.put(uri)
...
...     case ['DELETE', uri, 'HTTP/1.0']:  http10.delete(uri)
...     case ['DELETE', uri, 'HTTP/1.1']:  http11.delete(uri)
...     case ['DELETE', uri, 'HTTP/2.0']:  http20.delete(uri)
...     case ['DELETE', uri, 'HTTP/3.0']:  http30.delete(uri)

4.7.9. Use Case - 6

>>> def values(*data):
...     match data:
...         case [a, b, c]: pass
...         case [a, b]: c = 0
...         case [a]: b = 0; c = 0
...         case []: raise ValueError('Empty list')
...         case _: raise ValueError('Other error')
...     return a, b, c
>>> a, b, c = values(1, 2, 3)
>>>
>>> a
1
>>> b
2
>>> c
3
>>> a, b, c = values(1, 2)
>>>
>>> a
1
>>> b
2
>>> c
0
>>> a, b, c = values(1)
>>> a
1
>>> b
0
>>> c
0
>>> a, b, c = values()
Traceback (most recent call last):
ValueError: Empty list

4.7.10. Use Case - 7

>>> def range(*args):
...     match args:
...         case [start, stop, step]: pass
...         case [start, stop]: step = 1
...         case [stop]: start = 0; step = 1
...         case []: raise TypeError('myrange expected at least 1 argument, got 0')
...         case _: raise TypeError(f'myrange expected at most 3 arguments, got {len(args)}')

4.7.11. Use Case - 8

>>> def range(*args):
...     match args:
...         case [stop]:
...             start = 0
...             step = 1
...         case [start, stop]:
...             step = 1
...         case [start, stop, step]:
...             pass
...         case []:
...             msg = 'myrange expected at least 1 argument, got 0'
...             raise TypeError(msg)
...         case _:
...             msg = f'myrange expected at most 3 arguments, got {len(args)}'
...             raise TypeError(msg)

4.7.12. Use Case - 9

>>> class Astronaut:
...     def move(self, *how):
...         match how:
...             case ['left', value]:   hero.move_left(value)
...             case ['right', value]:  hero.move_right(value)
...             case ['up', value]:     hero.move_up(value)
...             case ['down', value]:   hero.move_down(value)
...             case _: raise RuntimeError('Invalid move')
...
...     def move_left(self, value):
...         print(f'Moving left by {value}')
...
...     def move_right(self, value):
...         print(f'Moving right by {value}')
...
...     def move_up(self, value):
...         print(f'Moving up by {value}')
...
...     def move_down(self, value):
...         print(f'Moving down by {value}')
>>> hero = Astronaut()
>>>
>>> hero.move('left', 1)
Moving left by 1
>>>
>>> hero.move('right', 2)
Moving right by 2
>>>
>>> hero.move('up', 3)
Moving up by 3
>>>
>>> hero.move('down', 4)
Moving down by 4

4.7.13. Use Case - 10

>>> class Request:
...     def __init__(self, request: str):
...         match request.split():
...             case ['GET',    path, 'HTTP/2.0']: self.get(path)
...             case ['POST',   path, 'HTTP/2.0']: self.post(path)
...             case ['PUT',    path, 'HTTP/2.0']: self.put(path)
...             case ['DELETE', path, 'HTTP/2.0']: self.delete(path)
...
...     def get(self, path):
...         self.response = f'Processing GET request for {path}'
...
...     def post(self, path):
...         self.response = f'Processing POST request for {path}'
...
...     def put(self, path):
...         self.response = f'Processing PUT request for {path}'
...
...     def delete(self, path):
...         self.response = f'Processing DELETE request for {path}'
...
...     def __repr__(self):
...         return self.response
>>> Request('POST /user/ HTTP/2.0')
Processing POST request for /user/
>>> Request('GET /user/mwatney/ HTTP/2.0')
Processing GET request for /user/mwatney/
>>> Request('PUT /user/mwatney/ HTTP/2.0')
Processing PUT request for /user/mwatney/
>>> Request('DELETE /user/mwatney/ HTTP/2.0')
Processing DELETE request for /user/mwatney/

4.7.14. Use Case - 11

  • HTTP Request

Test Setup:

>>> def handle_get(path): ...
>>> def handle_post(path): ...
>>> def handle_put(path): ...
>>> def handle_delete(path): ...

Use Case:

>>> request = 'GET /index.html HTTP/2.0'
>>>
>>> match request.split():
...     case ['GET', path, 'HTTP/2.0']:     handle_get(path)
...     case ['POST', path, 'HTTP/2.0']:    handle_post(path)
...     case ['PUT', path, 'HTTP/2.0']:     handle_put(path)
...     case ['DELETE', path, 'HTTP/2.0']:  handle_delete(path)

4.7.15. 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: Match Sequence Range
# - Difficulty: medium
# - Lines: 6
# - Minutes: 8

# %% English
# 1. Write own implementation of a built-in function `range()`
# 2. Note, that function does not take any keyword arguments
# 3. How to implement passing only stop argument
#    `myrange(start=0, stop=???, step=1)`?
# 4. Use sequence pattern and wildcard pattern
# 5. Run doctests - all must succeed

# %% Polish
# 1. Zaimplementuj własne rozwiązanie wbudowanej funkcji `range()`
# 2. Zauważ, że funkcja nie przyjmuje żanych argumentów nazwanych (keyword)
# 3. Jak zaimplementować możliwość podawania tylko końca
#    `myrange(start=0, stop=???, step=1)`?
# 4. Użyj sequence pattern i wildcard pattern
# 5. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - https://github.com/python/cpython/blob/main/Objects/rangeobject.c#LC75
# - `match args`
# - `case [a,b,c]`
# - `case [a,b]`
# - `case [a]`
# - `case []`
# - `case _`
# - `raise TypeError('error message')`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 10), \
'Python 3.10+ 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
"""


# myrange(start=0, stop=???, step=1)
# 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')

    match len(args):
        case 3:
            start = args[0]
            stop = args[1]
            step = args[2]
        case 2:
            start = args[0]
            stop = args[1]
            step = 1
        case 1:
            start = 0
            stop = args[0]
            step = 1
        case 0:
            raise TypeError('myrange expected at least 1 argument, got 0')
        case n:
            raise TypeError(f'myrange expected at most 3 arguments, got {n}')

    current = start
    result = []

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

    return result