<jupyter_start><jupyter_text>Two tricks and a pitfall  Trick 1: The for-else-loopPython allows a for-else-loop. This is used for for-loops with a breaking condition to check whether the breaking condition was met or the loop ran through. Example:We have a list of models `models = ['MESSAGE', 'GCAM', 'REMIND']` and want to find out if any model name is shorter than 4 characters. If we find one, we want to print the first one we find. If we do not fine one, we want to say that we came up empty handed.  Side Note:Of course the following example can be solved more efficiently using for example list comprehension. It is only meant to illustrate the point of a for-else-loop construct.<jupyter_code>models = ['MESSAGE', 'GCAM', 'REMIND']
for m in models:
    if len(m) < 4:
        print(f"Model '{m}' has length {len(m)}.")
        break
else:
    print("No model shorter than 4 characters found.")<jupyter_output>No model shorter than 4 characters found.<jupyter_text>Trick 2 f-stringsF-strings are pythons latest (since version 3.6 added trough [PEP 498](https://www.python.org/dev/peps/pep-0498/)) way of formatting strings. Here's a few short examples of what f-strings can do and why they might be useful for you:  Basic use* printing a variable value* in-place evaluation of expressions<jupyter_code>some_variable = 'some value'
x = 15.3
print(f'{some_variable}')
print(f'{17*x - 14}')<jupyter_output>some value
246.10000000000002<jupyter_text>Print variable name and value* useful for debugging and error messages<jupyter_code>print(f'{some_variable = }')
print(f'{17*x - 14 = }')
error_msg = f'There was a problem with {some_variable = }'
print(error_msg)<jupyter_output>some_variable = 'some value'
17*x - 14 = 246.10000000000002
There was a problem with some_variable = 'some value'<jupyter_text>Padding* useful for creating nicely formatted text output (to the console or to a file)<jupyter_code>x = 'test'
y = 'test 2'
# left
print(f'{x:>10}')
print(f'{y:>10}')
# right
print(f'{x:*<10}')
# both sides
print(f'{x:=^10}')
# fancy padding depending on a variable
n = 25
print(f'{x:~^{n}}')<jupyter_output>test
    test 2
test******
===test===
~~~~~~~~~~test~~~~~~~~~~~<jupyter_text>Number formatting and conversion* also useful for creating nicely formatted output<jupyter_code>a = 42
b = 42.19648624816541

# Number formatting
print(f"{a:04d}") # zero padded integer
print(f"{a:06.2f}") # zero padded floating point
print(f"{b:.1f}") # zero padded floating point, rounds automatically (**very handy**)

# Number conversion
print(f"{a:x}") # hex
print(f"{a:X}") # hex (uppercase)
print(f"{a:b}") # binary
print(f"{a:c}") # ascii
print(f"{a:o}") # octal
print(f"{a:010b}") # combined with padding, padding with 0 to a total of 10 digits<jupyter_output>0042
042.00
42.2
2a
2A
101010
*
52
0000101010<jupyter_text>The Pitfall: Mutable default argumentsThe python interpreter evaluates default arguments only **once** at the beginning of the interpretation process.Mutable default arguments don't change, even though we might expect them to.Mutable data types are:* lists* dictionaries* set* user-defined classes* results from function calls (see the datetime.now() example below)This can lead to unexpected beheavior where the defaults are not re-evaluated every time the function is called. For more details refer to [this explanation](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument) on stackoverflow. Side Note: As above both of the following items are a bit contrived. However, they are again simply meant to provide a simple illustration of the problem rather than give a realistic use-case in the wild.<jupyter_code># really simple function, appends a variable x to a list l 
# the list l is optional, if we don't provide one, the function
# takes an empty one.
def add_to_list(x, l=[]):
    l.append(x)
    return l

print(add_to_list(1)) # we would expect [1], we get [1]
print(add_to_list("hello")) # we would expect ["hello"], we get [1, "hello"]
from datetime import datetime
from time import sleep

# another really simple function which takes a datetime object
# and just prints it. Per default we want to print the current
# date and time

def print_time(dt: datetime = datetime.now()):
    print(dt)
    
print_time() # we print the time once
sleep(5)
# here we would expect the print to show a time 5 seconds later,
# however, we get the same time as before. This is because datetime.now()
# is only evaluated once.
print_time()<jupyter_output>2021-08-12 09:30:33.406968
2021-08-12 09:30:33.406968<jupyter_text>The solution: the 'if arg is None'-patternIn order to avoid the problem of mutable default arguments not resetting we need to reset them explicitly. For this we use the 'if arg is None'-pattern.```pythondef f(arg=None):    if arg is None:        arg =  whatever you want the default to be```<jupyter_code>from datetime import datetime
from time import sleep

# if arg is None pattern
def add_to_list(x, l: list = None) -> list:
    if l is None:
        l = []
    l.append(x)
    return l

def print_time(dt = None):
    if dt is None:
        dt = datetime.now()
    print(dt)

print(add_to_list(1))
print(add_to_list(2))

print_time()
sleep(5)
print_time()<jupyter_output>[1]
[2]
2021-08-12 09:30:38.445944
2021-08-12 09:30:43.451309