diff --git a/episodes/01-introduction.md b/episodes/01-introduction.md index 081eb91..73260c8 100644 --- a/episodes/01-introduction.md +++ b/episodes/01-introduction.md @@ -189,6 +189,63 @@ else: ::::::::::::::::::::::::::::::::::::::::::::::: +::::::::::::::::::::::::::::::::::::::::: callout + +## Formatting Variables and Objects in Strings + +Throughout this course, we will be printing the values of variables inside strings to demonstrate what the code does. There are a number of ways to this in Python; in this course we will be using `f-strings` which are the recommended string interpolation syntax from version 3.6 onwards. + +To use `f-strings` simply start a string literal with `f` or `F`, then embed whatever you want interpolating into the string within curly braces `{}`: + +```python +two_int = 2 +two_str = "2" +f_string = f"{two_int} + {two_str} = {2 + 2}" +print(f_string) +``` +```output +2 + 2 = 4 +``` + +Numerical values can be formatted using `:` inside the curly braces. For example, an integer `i` can be made to have a width `n` using `{i:nd}`, or `{i:0nd}` to pad it out with zeros: + +```python +print(f"One with four leading spaces {1:5d}") +print(f"One with four leading zeros {1:05d}") +``` +```output +One with four leading spaces 1 +One with four leading zeros 00001 +``` + +Floating point variables can have their precision specified with a number after a decimal point and an `f`: +```python +pi = 3.141592653589793 +print(pi) +print(f"pi to three decimal places is {pi:.3f}") +``` +```output +3.141592653589793 +pi to three decimal places is 3.142 +``` + +Or made to use scientific notation with an `e`: + +```python +print(1e9) +print(f"A billion in scientific notation is {1e9:.1e}") +``` +```output +1000000000.0 +A billion in scientific notation is 1.0e+09 +``` + +This is only scratching the surface of what you can do with `f-strings`, they are often the most powerful and concise way to format variables inside strings. However, there are occasions when other methods are preferable, and you should be careful using them with Python [before version 3.12](https://realpython.com/python-f-strings/#upgrading-f-strings-python-312-and-beyond). + +To learn more, see: [Python f-strings](https://realpython.com/python-f-strings/). + +:::::::::::::::::::::::::::::::::::::::::::::::::: + ## Generative AI We would like to reiterate the [Carpentries](https://carpentries.org/) stance on [Generative AI in coding](https://swcarpentry.github.io/python-novice-inflammation/01-intro.html#generative-ai) which you may have seen when preparing for this course. diff --git a/episodes/02-dictionaries.md b/episodes/02-dictionaries.md index 517c50e..2ccdfb8 100644 --- a/episodes/02-dictionaries.md +++ b/episodes/02-dictionaries.md @@ -117,9 +117,6 @@ Following the previous example, we can create a python dictionary using the name ```python d = {'alice': 35, 'bob': 18} -``` - -```python print(d) ``` @@ -150,7 +147,7 @@ d4 = dict(zip(['jane','alice','bob'],[24,35,18])) To access an element of the dictionary we must use the *key*: ```python -print('The age of alice is :', d['alice']) +print(f'The age of alice is: {d["alice"]}') ``` ```output @@ -161,8 +158,8 @@ We can also use a variable to index the dictionary: ```python key = 'alice' -print('The name of the person is used as key:', key) -print('The value associated to that key is:', d[key]) +print(f'The name of the person is used as key: {key}') +print(f'The value associated to that key is: {d[key]}') ``` ```output @@ -175,9 +172,9 @@ The value associated to that key is: 35 Adding an element to a dictionary is done by creating a new key and attaching a value to it. ```python -print('Original dictionary:', d) +print(f'Original dictionary: {d}') d['jane'] = 24 -print('New dictionary:', d) +print(f'New dictionary: {d}') ``` ```output @@ -191,7 +188,7 @@ To add one or more new elements we can also use the `update` method: d_extra = {'tom': 54, 'david': 87} d.update(d_extra) -print('Updated dictionary:', d) +print(f'Updated dictionary: {d}') ``` ```output @@ -202,7 +199,7 @@ To delete an element, use the `del` method: ```python del d['tom'] -print('Dictionary with item deleted:', d) +print(f'Dictionary with item deleted: {d}') ``` ```output @@ -213,8 +210,8 @@ Alternatively, the `pop` function can be used to take an element out of a dictio ```python david_age = d.pop('david') -print('Age of David:', david_age) -print('Dictionary with david popped out:', d) +print(f'Age of David: {david_age}') +print(f'Dictionary with david popped out: {d}') ``` ```output @@ -250,9 +247,9 @@ TypeError: unsupported operand type(s) for +: 'dict' and 'dict' Keys have to be unique; you cannot have two keys with the same name. If you try to add an item using a key already present in the dictionary you will overwrite the previous value. ```python -print('Original dictionary:', d) +print(f'Original dictionary: {d}') d['alice'] = 12 -print('New dictionary:', d) +print(f'New dictionary: {d}') ``` ```output @@ -273,9 +270,9 @@ d1 = {'alice': 12, 'bob': 18, 'jane': 24, 'tom': 54, 'david': 87} d2 = {'tom': 54, 'david': 87} d3 = {'bob': 18, 'alice': 35, 'jane': 24} d4 = {'alice': 35, 'bob': 18, 'jane': 24} -print('Dictionary 1 and dictionary 2 are equal:', d1 == d2) -print('Dictionary 1 and dictionary 3 are equal:', d1 == d3) -print('Dictionary 3 and dictionary 4 are equal:', d3 == d4) +print(f'Dictionary 1 and dictionary 2 are equal: {d1 == d2}') +print(f'Dictionary 1 and dictionary 3 are equal: {d1 == d3}') +print(f'Dictionary 3 and dictionary 4 are equal: {d3 == d4}') ``` ```output @@ -327,7 +324,7 @@ list(d.values())[0] ``` It is also possible to iterate through the keys and items in the dictionary at the same time using the `items` function, -which returns a *dict\_items* object containing `key, value` pairs: +which returns a *dict\_items* object containing *key, value* pairs: ```python d.items() @@ -341,7 +338,7 @@ This is very useful when using a dictionary in a `for` loop: ```python for key, value in d.items(): - print("Name:", key, " Age:", value) + print(f'Name: {key}, Age: {value}') ``` ```output diff --git a/episodes/03-numpy_essential.md b/episodes/03-numpy_essential.md index 1d93da8..f07fc1b 100644 --- a/episodes/03-numpy_essential.md +++ b/episodes/03-numpy_essential.md @@ -271,13 +271,12 @@ To identify any signal in the data we can use the standard deviation as an estim ```python stddev_noisy = np.std(noisy) mean_noisy = np.mean(noisy) -print(f'standard deviation is: {stddev_noisy}') -print(f'mean value is: {mean_noisy}') +print(f'standard deviation is: {stddev_noisy:.5f}') +print(f'mean value is: {mean_noisy:.5f}') ``` - ```output -standard deviation is: 0.011592652442611553 -mean value is: 0.005047252119578472 +standard deviation is: 0.01159 +mean value is: 0.00505 ``` We will create a mask for the data, by selecting all data points below this threshold value (we'll assume here that any signal we might be interested in is positive): @@ -394,10 +393,10 @@ To improve the visible output we will carry out some simple analysis of the imag First we examine the general stats of the data (using built-in methods, except for the median, which has to be called from NumPy directly): ```python -print('mean value im1:', imdata.mean()) -print('median value im1:', np.median(imdata)) -print('max value im1:', imdata.max()) -print('min value im1:', imdata.min()) +print(f'mean value im1: {imdata.mean()}') +print(f'median value im1: {np.median(imdata)}') +print(f'max value im1: {imdata.max()}') +print(f'min value im1: {imdata.min()}') ``` ```output @@ -482,17 +481,17 @@ plt.imshow(immasked.mask, cmap='gray', origin='lower') This mask is applied to the data for all built-in functions. But where we have to directly use a NumPy function we have to make sure we use the equivalent function in the mask (`ma`) library: ```python -print('original average:', imdata.mean()) -print('Masked average:', immasked.mean()) -print() -print('original max:', imdata.max()) -print('Masked max:', immasked.max()) -print() -print('original min:', imdata.min()) -print('Masked min:', immasked.min()) -print() -print('original median:', np.mean(imdata)) -print('Masked median:', np.ma.median(immasked)) +print(f'original average: {imdata.mean()}') +print(f'Masked average: {immasked.mean()}\n') + +print(f'original max: {imdata.max()}') +print(f'Masked max: {immasked.max()}\n') + +print(f'original min: {imdata.min()}') +print(f'Masked min: {immasked.min()}\n') + +print(f'original median: {np.mean(imdata)}') +print(f'Masked median: {np.ma.median(immasked)}') ``` ```output diff --git a/episodes/05-defensive_programming.md b/episodes/05-defensive_programming.md index 5722bf9..c85e28e 100644 --- a/episodes/05-defensive_programming.md +++ b/episodes/05-defensive_programming.md @@ -34,11 +34,11 @@ Please look at the following code. Can you find the fundamental problem in this val = 1 if val > 0: - print('Value:', val, 'is positive.') + print(f'Value: {val} is positive.') elif val == 0: - print('Value:', val, 'is zero.') + print(f'Value: {val} is zero.') else: - print('Value:', val, 'is negative.') + print(f'Value: {val} is negative.') ``` ::::::::::::::: solution @@ -51,11 +51,11 @@ The test assumes that `val` is a number, and throws an uncontrolled error if it val = 'a' if val > 0: - print('Value:', val, 'is positive.') + print(f'Value: {val} is positive.') elif val == 0: - print('Value:', val, 'is zero.') + print(f'Value: {val} is zero.') else: - print('Value:', val, 'is negative.') + print(f'Value: {val} is negative.') ``` ```output @@ -64,9 +64,9 @@ TypeError Traceback (most recent call last) in () 1 def check_sign(val): ----> 2 if val > 0: - 3 print('Value:', val, 'is positive.') + 3 print(f'Value: {val} is positive.') 4 elif val == 0: - 5 print('Value:', val, 'is zero.') + 5 print(f'Value: {val} is zero.') TypeError: '>' not supported between instances of 'str' and 'int' ``` @@ -81,11 +81,11 @@ To make things simpler, we will first write the test as a function: ```python def check_sign(val): if val > 0: - print('Value:', val, 'is positive.') + print(f'Value: {val} is positive.') elif val == 0: - print('Value:', val, 'is zero.') + print(f'Value: {val} is zero.') else: - print('Value:', val, 'is negative.') + print(f'Value: {val} is negative.') ``` Then wrap the function call in an `if` statement: @@ -121,7 +121,7 @@ try: except TypeError as err: print('Val is not a number') print('But our code does not crash anymore') - print('The run-time error is:', err) + print(f'The run-time error is: {err}') ``` As with `if` statements, multiple `except` statements can be used, each with a different error test. These can be followed by an (optional) catch-all bare `except` statement (as we started with) to catch any unexpected errors. Note that only one `try` statement is allowed in the structure. @@ -134,12 +134,12 @@ try: reciprocal = 1/val except TypeError as err: print('Val is not a number') - print('The run-time error is:', err) + print(f'The run-time error is: {err}') except Exception as err: print('Some error other than a TypeError occured') - print('The run-time error is:', err) + print(f'The run-time error is: {err}') else: - print('The reciprocal of the value =', reciprocal) + print(f'The reciprocal of the value = {reciprocal}') finally: print('release memory') ``` @@ -214,7 +214,7 @@ To conduct such a test we can use an `assert` statement. This follows the struct ```python val = 'a' -assert type(val) is float or type(val) is int, "Variable has to be a numerical object" +assert type(val) is float or type(val) is int, 'Variable has to be a numerical object' check_sign(val) ``` diff --git a/episodes/06-units_and_quantities.md b/episodes/06-units_and_quantities.md index 03f3cbd..dfa33f2 100644 --- a/episodes/06-units_and_quantities.md +++ b/episodes/06-units_and_quantities.md @@ -296,7 +296,7 @@ We can see from this that the degree unit is `u.deg`, so we can use this to defi ```python angle = 90 * u.deg -print('angle in degrees: {}; and in radians: {}'.format(angle.value,angle.to(u.rad).value)) +print(f'angle in degrees: {angle.value}; and in radians: {angle.to(u.rad).value}') ``` ```output @@ -306,9 +306,9 @@ angle in degrees: 90.0; and in radians: 1.5707963267948966 Now we can pass the angle directly to `np.sin` without having to convert directly to radians: ```python -print('sin of 90 degrees is: {}'.format(np.sin(angle))) -print('sin of pi/2 radians is: {}'.format(np.sin(1.57079632))) -print('sin of 90 degrees is not: {}'.format(np.sin(90))) +print(f'sin of 90 degrees is: {np.sin(angle)}') +print(f'sin of pi/2 radians is: {np.sin(1.57079632)}') +print(f'sin of 90 degrees is not: {np.sin(90)}') ``` ```output @@ -362,7 +362,8 @@ Each of the temperature scales is considered as using an irreducible unit in sta ```python t2 = 1 * u.deg_C -print('{} is equivalent to {}'.format(t2,t2.to(imperial.Fahrenheit, equivalencies=u.temperature()))) +t2_deg_f = t2.to(imperial.Fahrenheit, equivalencies=u.temperature()) +print(f'{t2} is equivalent to {t2_deg_f}') ``` ```output @@ -376,19 +377,16 @@ To get the incremental value of 1 degree Celsius in Fahrenheit we would need to ```python t1 = 0 * u.deg_C t2 = 1 * u.deg_C -print('{} increment is equivalent to a {} increment'.format( - t2 - t1, - t2.to(imperial.Fahrenheit,equivalencies=u.temperature()) - - t1.to(imperial.Fahrenheit, equivalencies=u.temperature()) - ) -) +t1_deg_f = t1.to(imperial.Fahrenheit, equivalencies=u.temperature()) +t2_deg_f = t2.to(imperial.Fahrenheit,equivalencies=u.temperature()) +print(f'{t2 - t1} increment is equivalent to a {t2_deg_f - t1_deg_f:.1f} increment') ``` ```output -1.0 deg_C increment is equivalent to a 1.7999999999999972 deg_F increment +1.0 deg_C increment is equivalent to a 1.8 deg_F increment ``` -This is verging on unreadable (as is demonstrated by us having to split a simple arithmetic expression involving two variables across two lines of code). +This is verging on unreadable (as is demonstrated by us having to split a simple arithmetic expression involving two variables across several lines of code). Fortunately there is support for cleaner temperature conversions in the [pint](https://pint.readthedocs.io/en/latest/index.html) package. This has similar functionality to the `astropy.unit` package, but has been built from the ground up with a focus on different aspects. These include unit parsing and standalone unit definitions, uncertainties integration, and (important for our example here), cleaner treatment of temperature units. @@ -411,22 +409,22 @@ This library has the temperature units as before: ```python t2 = 1 * ureg.degC -print('{} is equivalent to {}'.format(t2,t2.to(ureg.degF))) +print(f'{t2} is equivalent to {t2.to(ureg.degF):.1f}') ``` ```output -1 degree_Celsius is equivalent to 33.79999999999993 degree_Fahrenheit +1 degree_Celsius is equivalent to 33.8 degree_Fahrenheit ``` But it also includes the concept of temperature increments: ```python deltaT = 1 * ureg.delta_degC -print('{} is equivalent to {}'.format(deltaT,deltaT.to(ureg.delta_degF))) +print(f'{deltaT} is equivalent to {deltaT.to(ureg.delta_degF):.1f}') ``` ```output -1 delta_degree_Celsius is equivalent to 1.7999999999999998 delta_degree_Fahrenheit +1 delta_degree_Celsius is equivalent to 1.8 delta_degree_Fahrenheit ``` Because of the popularity of python this overlap of functionality of packages can be common. We would recommend exploring the different available packages that might cover your requirements before settling on one. And keep watching out for new packages, and be prepared to switch when starting new projects if/when you find better packages.