4.1. Doctest About
Tests are always the most up-to-date code documentation
Tests cannot get out of sync from code
Checks if function output is exactly as expected
Useful for regex modifications
Can add text (i.e. explanations) between tests
Use Cases:
4.1.1. Docstring
Docstring is a first multiline comment in: File/Module, Class, Method/Function
Used for generating
help()
documentationIt is accessible in
__doc__
property of an objectUsed for
doctest
PEP 257 -- Docstring Conventions: For multiline
str
always use three double quote ("""
) charactersMore information in Function Doctest
Docstring used for documentation:
>>> def say_hello():
... """This is the say_hello function"""
... print('Hello')
>>>
>>>
>>>
... help(say_hello)
Help on function say_hello in module __main__:
say_hello()
This is the say_hello function
>>>
>>> print(say_hello.__doc__)
This is the say_hello function
Docstring used for documentation:
>>> def say_hello():
... """
... This is the say_hello function
... And the description is longer then one line
... """
... print('Hello')
>>>
>>>
>>> help(say_hello)
Help on function say_hello in module __main__:
say_hello()
This is the say_hello function
And the description is longer then one line
>>>
>>> print(say_hello.__doc__)
This is the say_hello function
And the description is longer then one line
4.1.2. Syntax
Docstring is a first multiline comment in: File/Module, Class, Method/Function
Used for generating
help()
documentationIt is accessible in
__doc__
property of an objectUsed for
doctest
PEP 257 -- Docstring Conventions: For multiline
str
always use three double quote ("""
) characters
>>> def add(a, b):
... """
... >>> add(1, 2)
... 3
... >>> add(-1, 1)
... 0
... >>> add(0, 0)
... 0
... """
... return a + b
>>>
... """
... >>> add(1, 2)
... 3
... >>> add(-1, 1)
... 0
... >>> add(0, 0)
... 0
... """
>>>
>>> def add(a, b):
... return a + b
4.1.3. Running Tests
Running tests in Pycharm IDE (either option):
Right click on source code with doctests -> Run 'Doctest for ...'
View menu -> Run... -> Doctest in
myfunction
Note, that doctests are not discovered in scratch files in PyCharm
Running Tests from Python Code:
>>> if __name__ == "__main__":
... from doctest import testmod
... testmod()
Running tests from command line (displays errors only):
$ python -m doctest myfile.py
Add -v
to display more verbose output.
$ python -m doctest -v myfile.py
4.1.4. Test Int, Float
int
values:
>>>
... """
... >>> add(1, 2)
... 3
... >>> add(-1, 1)
... 0
... >>> add(0, 0)
... 0
... """
>>>
>>> def add(a, b):
... return a + b
float
values:
>>>
... """
... >>> add(1.0, 2.0)
... 3.0
...
... >>> add(0.1, 0.2)
... 0.30000000000000004
...
... >>> add(0.1, 0.2)
... 0.3000...
... """
>>>
>>> def add(a, b):
... return a + b
This is due to the floating point arithmetic in IEEE 754 standard:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2
0.3000...
>>> round(0.1+0.2, 16)
0.3
>>> round(0.1+0.2, 17)
0.30000000000000004
More information in Math Precision
4.1.5. Test Bool
>>>
... """
... Function checks if person is adult.
... Adult person is over 18 years old.
...
... >>> is_adult(18)
... True
...
... >>> is_adult(17.9)
... False
... """
>>>
>>> AGE_ADULT = 18
>>>
>>> def is_adult(age):
... if age >= AGE_ADULT:
... return True
... else:
... return False
4.1.6. Test Str
Python will change to single quotes in most cases
Python will change to double quotes to avoid escapes
print()
function output, don't have quotes
Returning str
. Python will change to single quotes in most cases:
>>>
... """
... >>> echo('hello')
... 'hello'
...
... # Python will change to single quotes in most cases
... >>> echo("hello")
... 'hello'
...
... Following test will fail
... >>> echo('hello')
... "hello"
...
... Python will change to double quotes to avoid escapes
... >>> echo('It\\'s Twardowski\\'s Moon')
... "It's Twardowski's Moon"
... """
>>>
>>> def echo(data):
... return data
There are no quotes in print()
function output:
>>>
... """
... >>> echo('hello')
... hello
... """
>>>
>>> def echo(data):
... print(data)
Testing print(str)
with newlines:
>>>
... """
... >>> echo('hello')
... hello
... hello
... hello
... <BLANKLINE>
... """
>>>
>>> def echo(data):
... print(f'{data}\n' * 3)
4.1.7. Test Ordered Sequence
>>>
... """
... >>> echo([1,2,3])
... [1, 2, 3]
...
... >>> echo((1,2,3))
... (1, 2, 3)
... """
>>>
>>> def echo(data):
... return data
>>>
... """
... >>> echo([1,2,3])
... [1, 2, 3]
...
... >>> echo((1,2,3))
... [1, 2, 3]
... """
>>>
>>> def echo(data):
... return [x for x in data]
>>>
... """
... >>> echo([1,2,3])
... [274.15, 275.15, 276.15]
...
... >>> echo((1,2,3))
... (274.15, 275.15, 276.15)
... """
>>>
>>> def echo(data):
... cls = type(data)
... return cls(x+273.15 for x in data)
4.1.8. Test Unordered Sequence
Hash from numbers are constant:
>>>
... """
... >>> echo({1})
... {1}
... >>> echo({1,2})
... {1, 2}
... """
>>>
>>> def echo(data):
... return data
However hash from str elements changes at every run:
>>>
... """
... >>> echo({'a', 'b'})
... {'b', 'a'}
... """
>>>
>>> def echo(data):
... return data
Therefore you should test if element is in the result, rather than comparing output:
>>>
... """
... >>> result = echo({'a', 'b'})
... >>> 'a' in result
... True
... >>> 'b' in result
... True
... """
>>>
>>> def echo(data):
... return data
4.1.9. Test Mapping
>>>
... """
... >>> result = echo({'a': 1, 'b': 2})
... >>> result
... {'a': 1, 'b': 2}
... >>> 'a' in result.keys()
... True
... >>> 1 in result.values()
... True
... >>> ('a', 1) in result.items()
... True
... >>> result['a']
... 1
... """
>>>
>>> def echo(data):
... return data
4.1.10. Test Nested
>>>
... """
... >>> DATA = [
... ... ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
... ... (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')]
...
... >>> echo(DATA)
... [('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'), (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')]
...
... >>> echo(DATA)
... [('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
... (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 echo(data):
... return data
4.1.11. Test Exceptions
>>>
... """
... >>> echo()
... Traceback (most recent call last):
... NotImplementedError
... """
>>>
>>> def echo():
... raise NotImplementedError
>>>
... """
... >>> echo()
... Traceback (most recent call last):
... NotImplementedError: This will work in future
... """
>>>
>>> def echo():
... raise NotImplementedError('This will work in future')
4.1.12. Test Type
>>>
... """
... >>> result = echo(1)
... >>> type(result)
... <class 'int'>
...
... >>> result = echo(1.1)
... >>> type(result)
... <class 'float'>
...
... >>> result = echo(True)
... >>> type(result)
... <class 'bool'>
...
... >>> result = echo([1, 2])
... >>> type(result)
... <class 'list'>
...
... >>> result = echo([1, 2])
... >>> any(type(x) is int
... ... for x in result)
... True
... """
>>>
>>> def echo(data):
... return data
The following doctest will fail:
>>>
... """
... >>> add_numbers('one', 'two')
... Traceback (most recent call last):
... TypeError: not a number
...
... >>> add_numbers(True, 1)
... Traceback (most recent call last):
... ValueError: not a number
... """
>>>
>>> def add_numbers(a, b):
... if not isinstance(a, (int, float)):
... raise ValueError('c')
... if not isinstance(b, (int, float)):
... raise ValueError('not a number')
... return a + b
Expected exception, got 2.0:
- Expected:
Traceback (most recent call last): ValueError: not a number
- Got:
2.0
This test will pass:
>>>
... """
... >>> add_numbers('one', 'two')
... Traceback (most recent call last):
... TypeError: not a number
...
... >>> add_numbers(True, 1)
... Traceback (most recent call last):
... ValueError: not a number
... """
>>>
>>> def add_numbers(a, b):
... if type(a) not in (int, float):
... raise ValueError('not a number')
... if type(b) not in (int, float):
... raise ValueError('not a number')
... return a + b
4.1.13. Test Python Expressions
Using python statements in doctest
:
>>> def echo(text):
... """
... >>> name = 'Mark Watney'
... >>> print(name)
... Mark Watney
... """
... return text
>>> def when(date):
... """
... >>> from datetime import datetime, timezone
... >>> moon = datetime(1969, 7, 21, 17, 54, tzinfo=timezone.utc)
... >>> when(moon)
... 1969-07-21 17:54 UTC
... """
... print(f'{date:%Y-%m-%d %H:%M %Z}')
4.1.14. Flags
DONT_ACCEPT_TRUE_FOR_1
DONT_ACCEPT_BLANKLINE
NORMALIZE_WHITESPACE
ELLIPSIS
IGNORE_EXCEPTION_DETAIL
SKIP
COMPARISON_FLAGS
REPORT_UDIFF
REPORT_CDIFF
REPORT_NDIFF
REPORT_ONLY_FIRST_FAILURE
FAIL_FAST
REPORTING_FLAGS
4.1.15. Case Study
Docstring used for doctest:
>>> def apollo_dsky(noun, verb):
... """
... This is the Apollo Display Keyboard
... It takes noun and verb
...
... >>> apollo_dsky(6, 61)
... Program selected. Noun: 06, verb: 61
...
... >>> apollo_dsky(16, 68)
... Program selected. Noun: 16, verb: 68
... """
... print(f'Program selected. Noun: {noun:02}, verb: {verb:02}')
Celsius to Kelvin conversion:
>>> def celsius_to_kelvin(data):
... """
... >>> celsius_to_kelvin([1,2,3])
... [274.15, 275.15, 276.15]
...
... >>> celsius_to_kelvin((1,2,3))
... [274.15, 275.15, 276.15]
... """
... return [x+273.15 for x in data]
>>> def celsius_to_kelvin(data):
... """
... >>> celsius_to_kelvin([1,2,3])
... [274.15, 275.15, 276.15]
...
... >>> celsius_to_kelvin((1,2,3))
... (274.15, 275.15, 276.15)
... """
... cls = type(data)
... return cls(x+273.15 for x in data)
Adding two numbers:
>>> def add_numbers(a, b):
... """
... >>> add_numbers(1, 2)
... 3.0
... >>> add_numbers(-1, 1)
... 0.0
... >>> add_numbers(0.1, 0.2)
... 0.3000...
... >>> add_numbers(1.5, 2.5)
... 4.0
... >>> add_numbers(1, 1.5)
... 2.5
... >>> add_numbers([1, 2], 3)
... Traceback (most recent call last):
... ValueError: not a number
...
... >>> add_numbers(0, [1, 2])
... Traceback (most recent call last):
... ValueError: not a number
...
... >>> add_numbers('one', 'two')
... Traceback (most recent call last):
... ValueError: not a number
...
... >>> add_numbers(True, 1)
... Traceback (most recent call last):
... ValueError: not a number
... """
... if type(a) not in (int, float):
... raise ValueError('not a number')
...
... if type(b) not in (int, float):
... raise ValueError('not a number')
...
... return float(a + b)
Celsius to Kelvin temperature conversion:
>>> def celsius_to_kelvin(celsius):
... """
... >>> celsius_to_kelvin(0)
... 273.15
...
... >>> celsius_to_kelvin(1)
... 274.15
...
... >>> celsius_to_kelvin(-1)
... 272.15
...
... >>> celsius_to_kelvin(-273.15)
... 0.0
...
... >>> celsius_to_kelvin(-273.16)
... Traceback (most recent call last):
... ValueError: Negative Kelvin
...
... >>> celsius_to_kelvin(-300)
... Traceback (most recent call last):
... ValueError: Negative Kelvin
...
... >>> celsius_to_kelvin(True)
... Traceback (most recent call last):
... TypeError: Argument must be: int, float or Sequence[int, float]
...
... >>> celsius_to_kelvin([0, 1, 2, 3])
... [273.15, 274.15, 275.15, 276.15]
...
... >>> celsius_to_kelvin({0, 1, 2, 3})
... {273.15, 274.15, 275.15, 276.15}
...
... >>> celsius_to_kelvin([0, 1, 2, -300])
... Traceback (most recent call last):
... ValueError: Negative Kelvin
...
... >>> celsius_to_kelvin([0, 1, [2, 3], 3])
... [273.15, 274.15, [275.15, 276.15], 276.15]
... """
... datatype = type(celsius)
...
... if type(celsius) in {list, tuple, set, frozenset}:
... return datatype(celsius_to_kelvin(x) for x in celsius)
...
... if datatype not in {int, float}:
... raise TypeError('Argument must be: int, float or Sequence[int, float]')
...
... kelvin = celsius + 273.15
...
... if kelvin < 0.0:
... raise ValueError('Negative Kelvin')
...
... return float(kelvin)
4.1.16. 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: Test Doctest Distance
# - Difficulty: easy
# - Lines: 21
# - Minutes: 13
# %% English
# 1. Write doctests to a functions which convert distance given in kilometers to meters
# 2. Valid arguments:
# - `int`
# - `float`
# 3. Invalid argumentm, raise exception `TypeError`:
# - `str`
# - `list[int]`
# - `list[float]`
# - `bool`
# - any other type
# 4. Returned distance must be float
# 5. Returned distance cannot be negative
# 6. Run doctests - all must succeed
# %% Polish
# 1. Napisz doctesty do funkcji, która przeliczy dystans podany w kilometrach na metry
# 2. Poprawne argumenty:
# - `int`
# - `float`
# 3. Niepoprawne argumenty, podnieś wyjątek `TypeError`:
# - `str`
# - `list[int]`
# - `list[float]`
# - `bool`
# - any other type
# 4. Zwracany dystans musi być float
# 5. Zwracany dystans nie może być ujemny
# 6. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - 1 km = 1000 m
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
"""
def km_to_meters(kilometers):
if type(kilometers) not in {int, float}:
raise TypeError('Invalid argument type')
if kilometers < 0:
raise ValueError('Argument must be not negative')
return float(kilometers * 1000)
# %% 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: Test Doctest Temperature
# - Difficulty: easy
# - Lines: 5
# - Minutes: 13
# %% English
# 1. Write doctests to `celsius_to_kelvin` function
# 2. Parameter `degrees` can be:
# - int
# - float
# - list[int|float]
# - tuple[int|float,...]
# - set[int|float]
# - In other case raise an exception: TypeError
# with message: "Invalid argument type"
# 3. Run doctests - all must succeed
# %% Polish
# 1. Napisz doctesty do funkcji `celsius_to_kelvin`
# 2. Parametr `degrees` może być:
# - int
# - float
# - list[int|float]
# - tuple[int|float,...]
# - set[int|float]
# - W przeciwnym wypadku podnieś wyjątek: TypeError
# z komunikatem: "Invalid argument type"
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction
"""
def celsius_to_kelvin(degrees):
if type(degrees) in (int, float):
if degrees < -273.15:
raise ValueError('Argument must be not less than -273.15')
return 273.15 + degrees
if type(degrees) in (list, tuple, set):
if any(degree < -273.15 for degree in degrees):
raise ValueError('Argument must be not less than -273.15')
cls = type(degrees)
return cls(x+273.15 for x in degrees)
raise TypeError('Invalid argument type')