<jupyter_start><jupyter_text>(code-advcd-best-practice)= Tools for Better Coding  IntroductionThis chapter covers the tools that will help you to write better code. This includes practical topics such as debugging code, logging, linting, and the magic of auto-formatting.As ever, you may need to `conda install packagename` or `pip install packagename` on the terminal before being able to use some of the packages that are featured.  Auto-magically improving your codeIn the previous chapter, we met the idea of *code style*: even for code that runs, *how* you write it matters for readability. (And it goes without saying that you don't want bugs in your code that stop it running at all.) It is possible to catch some errors, to flag style issues, and even to re-format code to comply with a code style automatically. In this section, we'll see how to use tools to perform these functions automatically. LintingLinters are tools that analyse code for programmatic and stylistic errors, assuming you have declared a style. A linting tool flags any potential errors and deviations from style before you even run it. When you run a linter, you get a report of what line the issue is on and why it has been raised. They are supposedly named after the lint trap in a clothes dryer because of the way they catch small errors that could have big effects.Some of the most popular linters in Python are [**flake8**](https://flake8.pycqa.org/en/latest/), [**pycodestyle**](https://pycodestyle.pycqa.org/en/latest/intro.html), and [**pylint**](https://www.pylint.org/).Let's see an example of running a linter. VS Code has direct integration with a range of linters. To get going, use `⇧⌘P` (Mac) and then type 'Python Select Linter'. In the example below, we'll use **flake8** (and **pylance**, another VS Code extension). Let's pretend we have a script, `test.py`, containing```pythonlist_defn = [1,5, 6,7]def this_is_a_func():   print('hello')print(X)import numpy as np```To see the linting report, press ^ + \` (Mac) or ctrl + ` (otherwise) and navigate to the 'Problems' tab. We get a whole load of error messages about this script, here are a few:- ⓧ missing whitespace after ',' flake8(E231) 1, 15- ⓧ continuation line under-indented for visual indent flake8(E128) 2, 1- ⓧ expected 2 blank lines, found 1 flake8(E302) 4, 1- ⓧ indentation is not a multiple of 4 flake8(E111) 5, 4- ⓧ undefined name 'X' flake8(F821) 7, 7- ⓧ module level import not at top of file flake8(E402) 9, 1- ⓧ 'numpy as np' imported but unused flake8(F3401) 9, 1- ⚠ "X" is not defined Pylance(reportUndefinedVariable) 7, 7- ⚠ no newline at end of file flake8(W292) 78, 338each message is a warning or error that says what the problem is (for example, missing whitespace after ','), what library is reporting it (mostly flake8 here), the name of the rule that has been broken (E231), and the line, row position (1, 15). Very helpfully, we get an undefined name message for variable `X`, this is especially handy because it would cause an error on execution otherwise. The same goes for the indentation message (indentation matters!). You can customise your [linting settings](https://code.visualstudio.com/docs/python/linting) in VS Code too.Although the automatic linting offered by an IDE is very convenient, it's not the only way to use linting tools. You can also run them from the command line. For example, for **flake8**, the command is `flake8 test.py`. FormattingIt's great to find out all the ways in which you are failing with respect to code style from a linter but wouldn't it be *even* better if you could fix those style issues automatically? The answer is clearly yes! This is where formatters come in; they can take valid code and forcibly apply a code style to them. This is really handy in practice for all kinds of reasons.The most popular code formatters in Python are probably: [**yapf**](https://github.com/google/yapf), 'yet another Python formatter', from Google; [**autopep8**](https://github.com/hhatto/autopep8), which applies PEP8 to your code; and [**black**](https://black.readthedocs.io/en/stable/), the 'uncompromising formatter' that is very opinionated ("any colour, as long as it's black").There are two ways to use formatters, line-by-line (though **black** doesn't work in this mode) or on an entire script at once. VS Code offers an integration with formatters. To select a formatter in VS Code, bring up the settings using ⌘ + , (Mac) or ctrl + , (otherwise) and type 'python formatting provider' and you can choose from autopep8, black, and yapf. If you choose **autopep8** and then open a script you can format a *selection* of code by pressing ⌘ + k, ⌘ + f (Mac) or ctrl + k, ctrl + f (otherwise). They can also (and only, in the case of **black**) be used from the command line. For instance, to use **black**, the command is `black test.py`, assuming you have it installed.Let's see an example of a poorly styled script and see what happens when we select all lines and use ctrl + k, ctrl + f to auto format with **autopep8**. The contents of `test.py` before formatting are:```pythondef very_important_function(y_val,debug = False, keyword_arg=0, another_arg  =2):    X = np.linspace(0,10,5)    return X+  y_val +keyword_argvery_important_function(2)list_defn = [1,2,3,5,6,7]import numpy as np```and, after running the auto-formatting command,```pythonimport numpy as npdef very_important_function(y_val, debug=False, keyword_arg=0, another_arg=2):    X = np.linspace(0, 10, 5)    return X + y_val + keyword_argvery_important_function(2)list_defn = [1,             2,             3,             5,             6,             7]```So what did the formatter do? Many things. It moved the import to the top, put two blank lines after the function definition, removed whitespace around keyword arguments, added a new line at the end, and fixed some of the indentation. The different formatters have different strengths and weaknesses; for example, **black** is not so good at putting imports in the right place but excels at splitting up troublesome wide lines. If you need a formatter that deals specifically with module imports, check out [**isort**](https://pycqa.github.io/isort/).Apart from taking the pressure off you to always be thinking about code style, formatters can be useful when working collaboratively too. For some open source packages, maintainers ask that new code or changed code be run through a particular formatter if it is to be incorporated into the main branch. This helps ensure the code style is consistent regardless of who is writing it. Running the code formatter can even be automated to happen every time someone *commits* some code to a shared code repository too, using something called a *pre-commit hook*.There is a package that can run **Black** on Jupyter Notebooks too: [**black-nb**](https://pypi.org/project/black-nb/).  Debugging codeComputers are *very* literal, so literal that unless you're perfectly precise about what you want, they will end up doing something different. When that happens, one of the most difficult issues in programming is to understand *why* the code isn't doing what you expected. When the code doesn't do what we expect, it's called a bug.Bugs could be fundamental issues with the code you're using (in fact, the term originated because a moth causing a problem in an early computer) and, if you find one of these, you should file an issue with the maintainers of the code. However, what's much more likely is that the instructions you gave aren't quite what is needed to produce the outcome that you want. And, in this case, you might need to *debug* the code: to find out which part of it isn't doing what you expect.Even with a small code base, it can be tricky to track down where the bug is: but don't fear, there are tools on hand to help you find where the bug is. Print statementsThe simplest, and I'm afraid to say the most common, way to debug code is to plonk `print` statements in the code. Let's take a common example in which we perform some simple array operations, here multiplying an array and then summing it with another array: ```pythonimport numpy as npdef array_operations(in_arr_one, in_arr_two):    out_arr = in_arr_one*1.5    out_arr = out_arr + in_arr_two    return out_arrin_vals_one = np.array([3, 2, 5, 16, '7', 8, 9, 22])in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])result = array_operations(in_vals_one, in_vals_two)result``````python---------------------------------------------------------------------------UFuncTypeError                            Traceback (most recent call last) in      11 in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])     12 ---> 13 result = array_operations(in_vals_one, in_vals_two)     14 result in array_operations(in_arr_one, in_arr_two)      3       4 def array_operations(in_arr_one, in_arr_two):----> 5     out_arr = in_arr_one*1.5      6     out_arr = out_arr + in_arr_two      7     return out_arrUFuncTypeError: ufunc 'multiply' did not contain a loop with signature matching types (dtype(' dtype('<U32')``` Oh no! We've got a `UFuncTypeError` here, perhaps not the most illuminating error message we've ever seen. We'd like to know what's going wrong here. The `Traceback` did give us a hint about where the issue occurred though; it happens in the multiplication line of the function we wrote.To debug the error with print statements, we might re-run the code like this: ```pythondef array_operations(in_arr_one, in_arr_two):    print(f'in_arr_one is {in_arr_one}')    out_arr = in_arr_one*1.5    out_arr = out_arr + in_arr_two    return out_arrin_vals_one = np.array([3, 2, 5, 16, '7', 8, 9, 22])in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])result = array_operations(in_vals_one, in_vals_two)result``````in_arr_one is ['3' '2' '5' '16' '7' '8' '9' '22']``````python---------------------------------------------------------------------------UFuncTypeError                            Traceback (most recent call last) in       9 in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])     10 ---> 11 result = array_operations(in_vals_one, in_vals_two)     12 result in array_operations(in_arr_one, in_arr_two)      1 def array_operations(in_arr_one, in_arr_two):      2     print(f'in_arr_one is {in_arr_one}')----> 3     out_arr = in_arr_one*1.5      4     out_arr = out_arr + in_arr_two      5     return out_arrUFuncTypeError: ufunc 'multiply' did not contain a loop with signature matching types (dtype(' dtype('<U32')``` What can we tell from the values of `in_arr_one` that are now being printed? Well, they seem to have quote marks around them and what that means is that they're strings, *not* floating point numbers or integers! Multiplying a string by 1.5 doesn't make sense here, so that's our error. If we did this, we might then trace the origin of that array back to find out where it was defined and see that instead of `np.array([3, 2, 5, 16, 7, 8, 9, 22])` being declared, we have `np.array([3, 2, 5, 16, '7', 8, 9, 22])` instead and `numpy` decides to cast the whole array as a string to ensure consistency.Let's fix that problem by turning `'7'` into `7` and run it again: ```pythondef array_operations(in_arr_one, in_arr_two):    out_arr = in_arr_one*1.5    out_arr = out_arr + in_arr_two    return out_arrin_vals_one = np.array([3, 2, 5, 16, 7, 8, 9, 22])in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])result = array_operations(in_vals_one, in_vals_two)result``````python---------------------------------------------------------------------------ValueError                                Traceback (most recent call last) in       8 in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])      9 ---> 10 result = array_operations(in_vals_one, in_vals_two)     11 result in array_operations(in_arr_one, in_arr_two)      1 def array_operations(in_arr_one, in_arr_two):      2     out_arr = in_arr_one*1.5----> 3     out_arr = out_arr + in_arr_two      4     return out_arr      5 ValueError: operands could not be broadcast together with shapes (8,) (7,) ``` Still not working! But we've moved on to a different error now. We can still use a print statement to debug this one, which seems to be related to the shapes of variables passed into the function: ```pythondef array_operations(in_arr_one, in_arr_two):    print(f'in_arr_one shape is {in_arr_one.shape}')    out_arr = in_arr_one*1.5    print(f'intermediate out_arr shape is {out_arr.shape}')    print(f'in_arr_two shape is {in_arr_two.shape}')    out_arr = out_arr + in_arr_two    return out_arrin_vals_one = np.array([3, 2, 5, 16, 7, 8, 9, 22])in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])result = array_operations(in_vals_one, in_vals_two)result``````in_arr_one shape is (8,)intermediate out_arr shape is (8,)in_arr_two shape is (7,)``````python---------------------------------------------------------------------------ValueError                                Traceback (most recent call last) in      11 in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])     12 ---> 13 result = array_operations(in_vals_one, in_vals_two)     14 result in array_operations(in_arr_one, in_arr_two)      4     print(f'intermediate out_arr shape is {out_arr.shape}')      5     print(f'in_arr_two shape is {in_arr_two.shape}')----> 6     out_arr = out_arr + in_arr_two      7     return out_arr      8 ValueError: operands could not be broadcast together with shapes (8,) (7,) ``` The print statement now tells us the shapes of the arrays as we go through the function. We can see that in the line before the `return` statement the two arrays that are being combined using the `+` operator don't have the same shape, so we're effectively adding two vectors from two differently dimensioned vector spaces and, understandably, we are being called out on our nonsense. To fix this problem, we would have to ensure that the input arrays are the same shape (it looks like we may have just missed a value from `in_vals_two`).`print` statements are great for a quick bit of debugging and you are likely to want to use them more frequently than any other debugging tool. However, for complex, nested code debugging, they aren't always very efficient and you will sometimes feel like you are playing battleships in continually refining where they should go until you have pinpointed the actual problem, so they're far from perfect. Fortunately, there are other tools in the debugging toolbox...  Icecream and better print statementsTyping `print` statements with arguments that help you debug code can become tedious. There are better ways to work, which we'll come to, but we must also recognise that `print` is used widely in practice. So what if we had a function that was as easier to use as `print` but better geared toward debugging? Well, we do, and it's called [**icecream**](https://github.com/gruns/icecream), and it's available in most major languages, including Python, Dart, Rust, javascript, C++, PHP, Go, Ruby, and Java. Let's take an example from earlier in this chapter, where we used a `print` statement to display the contents of `in_arr_one` in advance of the line that caused an error being run. All we will do now is switch out `print(f'in_arr_one is {in_arr_one}')` for `ic(in_arr_one)`. ```pythonfrom icecream import icdef array_operations(in_arr_one, in_arr_two):     Old debug line using `print`     print(f'in_arr_one is {in_arr_one}')     new debug line:    ic(in_arr_one)    out_arr = in_arr_one*1.5    out_arr = out_arr + in_arr_two    return out_arrin_vals_one = np.array([3, 2, 5, 16, '7', 8, 9, 22])in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])array_operations(in_vals_one, in_vals_two)``````ic| in_arr_one: array(['3', '2', '5', '16', '7', '8', '9', '22'], dtype='<U21')---------------------------------------------------------------------------UFuncTypeError                            Traceback (most recent call last) in      14 in_vals_two = np.array([4, 7, 3, 23, 6, 8, 0])     15 ---> 16 array_operations(in_vals_one, in_vals_two) in array_operations(in_arr_one, in_arr_two)      6      new debug line:      7     ic(in_arr_one)----> 8     out_arr = in_arr_one*1.5      9     out_arr = out_arr + in_arr_two     10     return out_arrUFuncTypeError: ufunc 'multiply' did not contain a loop with signature matching types (dtype(' dtype('<U32')``` What we get in terms of debugging output is `ic| in_arr_one: array(['3', '2', '5', '16', '7', '8', '9', '22'], dtype='<U21')`, which is quite similar to before apart from three important differences, all of which are advantages:1. it is easier and quicker to write `ic(in_arr_one)` than `print(f'in_arr_one is {in_arr_one}')`2. **icecream** automatically picks up the name of the variable, `in_arr_one`, and clearly displays its contents3. **icecream** shows us that `in_arr_one` is of `type` array and that it has the `dtype` of `U`, which stands for Unicode (i.e. a string). `<U21` just means that all strings in the array are less than 21 characters long.**icecream** has some other advantages relative to print statements too, for instance it can tell you about which lines were executed in which scripts if you call it without arguments: ```pythondef foo():    ic()    print('first')        if 10 < 20:        ic()        print('second')    else:        ic()        print('Never executed')foo()``````ic| :2 in foo() at 00:58:19.962ic| :6 in foo() at 00:58:19.979firstsecond``` And it can wrap assignments rather than living on its own lines: ```pythondef half(i):    return ic(i) / 2a = 6b = ic(half(a))``````ic| i: 6ic| half(a): 3.0``` All in all, if you find yourself using `print` to debug, you might find a one-time import of **icecream** followed by use of `ic` instead both more convenient and more effective.  **rich** for beautiful debugging[**rich**](https://github.com/willmcgugan/rich) is much more than just a tool for debugging, it's a way to create beautiful renderings of all objects both in the terminal and in interactive Python windows. You can use it to build fantastic looking command line interfaces. Here, we'll see how it can help us debug what value a variable takes *and* what methods can be used on a variable via its `inspect` function.<jupyter_code>from rich import inspect

my_list = ["foo", "bar"]
inspect(my_list, methods=True)<jupyter_output><empty_output><jupyter_text>Check out all of the many options for `inspect` using `help(inspect)`. We ran it here with `methods=True`, but there are plenty of other options.```{admonition} ExerciseCreate a dictionary (it doesn't matter what's in it, but you could map the integer 1 to the letter "a"). Then use `inspect` with `methods=True` to find out about all the methods you can call on an object of type dict.```  Debugging with the IDEIn this section, we'll learn about how your Integrated Development Environment, or IDE, can aid you with debugging. While we'll talk through the use of Visual Studio Code, which is free, directly supports Python, R, and other languages, and is especially rich, many of the features will be present in other IDEs too and the ideas are somewhat general. To begin debugging using Visual Studio Code, get a script ready, for example `script.py`, that you'd like to debug. If your script has an error in, a debug run will automatically run into it and stop on the error; alternatively you can click to the left of the line number in your script to create a *breakpoint* that your code will stop at anyway when in debug mode.To begin a debug session, click on the play button partially covered by a bug that's on the left hand ribbon of the VS Code window. It will bring up a menu. Click 'Run and debug' and select 'Python file'. The debugger will now start running the script you had open. When it reaches and error or a breakpoint it will stop. Why is this useful? Once the code stops, you can hover over any variables and see what's 'inside' them, which is useful for working out what's going on. Remember, in the examples above, we only saw variables that we asked for. Using the debugger, we can hover over any variable we're interested in without having to decide ahead of time! We can also see other useful bits of info such as the *call stack* of functions that have been called, what local (within the current scope) and global (available everywhere) variables have been defined, and we can nominate variables to watch too.Perhaps you now want to progress the code on from a breakpoint; you can do this too. You'll see that a menu has appeared with stop, restart, play, and other buttons on it. To skip over the next line of code, use the curved arrow over the dot. To dig in to the next line of code, for example if it's a function, use the arrow pointing toward a dot. To carry on running the code, use the play button.This is only really scratching the surface of what you can do with IDE based debugging, but even that surface layer provides lots of really useful tools for finding out what's going on when your code executes.You can find a short 'hello world!' debugging tutorial in the [official VS Code documentation](https://code.visualstudio.com/docs/python/python-tutorial_configure-and-run-the-debugger).  LoggingLogging is a means of tracking events that happen when software runs. An event is described by a descriptive message that can optionally contain data about variables that are defined as the code is executing.Logging has two main purposes: to record events of interest, such as an error, and to act as an auditable account of what happened after the fact.Although Python has a built-in logger, we will see an example of logging using [**loguru**](https://github.com/Delgan/loguru), a package that makes logging a little easier and has some nice settings.Let's see how to log a debug message:<jupyter_code>from loguru import logger

logger.debug("Simple logging!")<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup