{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "(chpt-style)=\n", "# Code Style and Conventions\n", "\n", "Make your code consistent through style conventions. For interactive reading and executing code blocks [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/hydro-informatics/jupyter-python-course/main) and find *b08-pystyle.ipynb*, or install {ref}`Python ` and {ref}`JupyterLab ` locally.\n", "\n", "```{admonition} Watch this section as a video\n", ":class: tip, dropdown\n", "\n", "

Watch this section as a video on the @Hydro-Morphodynamics channel on YouTube.

\n", "```\n", "\n", "Take a deep breath, take off, and look at what you have learned so far from a new perspective. After this chapter, it will be worth having another look at your old code and formatting it robustly. The style guidelines presented here go beyond visual aesthetics and aid in writing effective code.\n", "\n", "```{figure} ../img/python/style-loop.png\n", ":alt: incubinate to solve Python programming problems\n", ":name: style-loop\n", "\n", "```\n", "\n", "## Background and PEP\n", "\n", "This style guide highlights parts of the [PEP 8 - Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/) by Guido van Rossum, Barry Warsaw, and Nick Coghlan. The full document is available at [python.org](https://www.python.org/dev/peps/pep-0008/) and only aspects with relevance for the applications shown in this eBook are featured in this chapter.\n", "\n", "**What is *PEP*?** *PEP* stands for ***P**ython **E**nhancement **P**roposals*, in which Python developers communicate features and developments of Python. At the time of writing these lines, there are twelve (minus two) PEPs dedicated to the development of Python modules, bug fix releases, and style guides (read the full and current list of PEPs at [python.org](https://www.python.org/dev/peps/#id6)). This section features recommendations stated in *PEP* 8, the style guide for Python code.\n", "\n", "Many IDEs, including PyCharm or Atom, provide auto-completion and tooltips with *PEP* style guidance to aid consistent programming. Thus, when your IDE underlines anything in your script, check the reason for that and consider modifying the code accordingly." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The Zen of Python\n", "\n", "Are we getting spiritual? Far from it. [The Zen of Python](https://www.python.org/dev/peps/pep-0020/) is an informational *PEP* (20) by Tim Peters to guide programmers. It is a couple of lines summarizing good practice in coding. Python's *Easter Egg* `import this` prints the Zen of Python in any Python interpreter:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The Zen of Python, by Tim Peters\n", "\n", "Beautiful is better than ugly.\n", "Explicit is better than implicit.\n", "Simple is better than complex.\n", "Complex is better than complicated.\n", "Flat is better than nested.\n", "Sparse is better than dense.\n", "Readability counts.\n", "Special cases aren't special enough to break the rules.\n", "Although practicality beats purity.\n", "Errors should never pass silently.\n", "Unless explicitly silenced.\n", "In the face of ambiguity, refuse the temptation to guess.\n", "There should be one-- and preferably only one --obvious way to do it.\n", "Although that way may not be obvious at first unless you're Dutch.\n", "Now is better than never.\n", "Although never is often better than *right* now.\n", "If the implementation is hard to explain, it's a bad idea.\n", "If the implementation is easy to explain, it may be a good idea.\n", "Namespaces are one honking great idea -- let's do more of those!\n" ] } ], "source": [ "import this" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Code Layout\n", "\n", "### Maximum Line Length\n", "\n", "The maximum length of a line is 79 characters and in-line comments, including {ref}`docstrings `, should not exceed 72 characters." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(py-indent-style)=\n", "### Indentation\n", "Indentation designates the sifting of code (blocks) to the right. Indentation is necessary, for example, in loops or functions to assign code blocks to a `for` or `def` statement. In a broader sense, indentations represent namespaces, where *local* variables defined in the indented region are valid only here. Multiple levels of indentation occur when nested statements are used (e.g., an `if` condition nested in a `for` loop). One level of indentation corresponds to 4 spaces." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "I'm one level indented.\n", "I'm two levels indented.\n" ] } ], "source": [ "for i in range(1,2):\n", " print(\"I'm one level indented.\")\n", " if i == 1:\n", " print(\"I'm two levels indented.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because long lines of code are bad practice, we sometimes need to use line breaks when assigning for example a *list* or calling a function. In these cases, the next, continuing is also indented and there are different options to indent multi-line assignments. Here, we want to use the style code of using an opening delimiter for indentation:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "a_too_long_word_list = [\"Do\", \"not\", \"hard-code\", \"something\", \"like\", \"this.\",\n", " \"There\", \"are\", \"better\", \"ways.\"]\n", "a_better_indented_list = [\n", " \"Do\",\n", " \"not\",\n", " \"hard-code\",\n", " \"something\",\n", " \"like\",\n", " \"this.\",\n", " \"...\",\n", " ]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Recall: PyCharm, Atom, and many other IDEs automatically layout indentation.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Line Breaks of Expressions with Binary Operators\n", "When binary operators are part of an expression that exceeds the maximum line length of 79 characters, the line break should be before the binary operators." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " parameter2 sensor3 variable1\n", "0 0 0 1\n", "1 1 0 0\n", "2 0 1 0\n" ] } ], "source": [ "dummy_df = pd.get_dummies(pd.Series(['variable1', 'parameter2', 'sensor3']))\n", "print(dummy_df.head(3))\n", "\n", "dum_sum = (dummy_df['variable1']\n", " + dummy_df['parameter2']\n", " - dummy_df['sensor3'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Blank Lines\n", "To separate code blocks, hitting the *Enter* key many times is a very inviting option. However, the random and mood-driven use of blank lines results in unstructured code. This is why *PEP* 8 authors provide guidance also on the use of blank lines:\n", "\n", "* Surround class definitions and top-level functions (i.e., functions where the `def`-line is not indented) with two blank lines.\n", "* Surround methods (e.g., functions within a class) with one blank line.\n", "* Use blank lines sparsely in all other code to indicate logical sections." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# blank 1 before top-level function\n", "# blank 2 before top-level function\n", "def top_level_function():\n", " pass\n", "# blank 1 after top-level function\n", "# blank 2 after top-level function" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Blanks (Whitespaces)\n", "\n", "Whitespaces aid to relax the code layout, but too many **whitespaces should be avoided** as for example:\n", "\n", "* In parentheses, brackets or braces (no: `list( e1, e2 )` vs. yes: `list(e1, e2)`)\n", "* In parentheses with tailing commas (no: `a_tuple = (1, )` vs. yes: `a_tuple = (1,)`)\n", "* Immediately before a comma\n", "* Between function name and argument parentheses (no: `fun (arg)` vs. yes: `fun(arg)`) and similar for list or dictionary elements\n", "* Around the `=` sign of unannotated function parameters indicating a default value (no: `def fun(arg = 0.0)` vs. yes: `def fun(arg=0.0)`)\n", "* Before `:` unless parentheses or brackets follow the `:` (e.g., `a_dict = {a_key: a_value}`)\n", "\n", "**Whitespaces should be added**:\n", "\n", "* Around any operator, boolean, or (augmented) assignment (e.g., `==, <, >, !=, <>, <=, >=, in, not in, is, is not, and, or, not, +=, -=`)\n", "* After colons `:` if a value antecedes the `:` and no parentheses or brackets follow immediately after the `:` (e.g., `a_dict = {a_key: a_value}`)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(libs)=\n", "## Packages and Modules\n", "\n", "### Imports\n", "Imports are at the top of the script, right after any {ref}`docstrings ` or other module comments. Import libraries first, then third-party packages, and lastly locally stored (own) modules. Preferably use absolute imports (e.g., `import package.module` or `from package import module`) and avoid wildcard imports (`from module import *`). Every import should have its own line and avoid using the comma sign for multiple imports:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# DO:\n", "import os\n", "import numpy as np\n", "# DO NOT:\n", "import os, sys" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Naming Packages and Script\n", "\n", "New, custom packages or modules should have short and all-lowercase names, where underscores may be used to improve readability (discouraged for packages).\n", "\n", "```{important}\n", "Never use a minus `-` sign in a Python file name, because the minus sign may cause import errors.\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Comments\n", "\n", "### Block and Inline Comments\n", "Block comments start with a single `#` at the first place of a line, followed by a whitespace and the comment text.\n", "\n", "Inline comments follow an expression and are indented with two whitespaces. However, the usage of inline comments is deprecated (i.e., do not use them or be sparse on their usage)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(docstrings)=\n", "### Docstrings\n", "\n", "Docstrings are short text descriptions within a module, function, class, or method with specifications of arguments, usage, and output. When instantiating a standard object, or referencing a class method, the `__doc__` attribute will print the object's docstring information. For example: " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Built-in mutable sequence.\n", "\n", "If no argument is given, the constructor creates a new empty list.\n", "The argument must be an iterable if specified.\n" ] } ], "source": [ "a_list = [1, 2]\n", "print(a_list.__doc__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When writing a Python function, docstrings are introduced immediately after the `def ...` line with triple double-apostrophes:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " Bright function accepting any input argument with indifferent behavior.\n", " :param an_input_argument: STR or anything else\n", " :param another_input_argument: FLOAT or anything else\n", " :return: True (in all cases)\n", " \n" ] } ], "source": [ "def let_there_be_light(*args, **kwargs):\n", " \"\"\"\n", " Bright function accepting any input argument with indifferent behavior.\n", " :param an_input_argument: STR or anything else\n", " :param another_input_argument: FLOAT or anything else\n", " :return: True (in all cases)\n", " \"\"\"\n", " print(\"Sunrise\")\n", " return True\n", "\n", "print(let_there_be_light.__doc__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the recommendations on docstrings are provided with [*PEP* 257](https://www.python.org/dev/peps/pep-0257/) rather than *PEP* 8." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Name Conventions\n", "\n", "### Definition of Name Styles\n", "\n", "The naming conventions use the following styles (source: [python.org](https://www.python.org/dev/peps/pep-0008/#naming-conventions)):\n", "\n", "* `b` (single lowercase letter)\n", "* `B` (single uppercase letter)\n", "* `lowercase`\n", "* `lower_case_with_underscores`\n", "* `UPPERCASE`\n", "* `UPPER_CASE_WITH_UNDERSCORES`\n", "* `CamelCase` or `CapWords` or `CapitalizedWords` or `StudlyCaps`.
Note: When using acronyms in `CapWords`, capitalize all the letters of the acronym (e.g., `HTTPResponse` is better than `HttpResponse`).\n", "* `mixedCase` (differs from `CapitalizedWords` by initial lowercase character!)\n", "* `Capitalized_Words_With_Underscores` (deprecated)\n", "\n", "Some variable name formats trigger a particular behavior of Python:\n", "\n", "* `_single_leading_underscore` variables indicate weak internal use and will not be imported with `from module import *`\n", "* `__double_leading_underscore` variables invoke name mangling in classes (e.g., a method called `__dlu` of the class `MyClass` will be mangled into `_MyClass__dlu`)\n", "* `__double_leading_and_tailing_underscore__` variables are *magic* objects or attributes in user-controlled namespaces (e.g., `__init__` or `__call__` in classes)
Only use documented magic attributes and never invent them. Read more about magic methods in the chapter on {ref}`Python classes `.\n", "* `single_tailing_underscore_` variables are used to avoid conflicts with Python keywords (e.g., `MyClass(class_='AnotherClass')`)\n", "\n", "(object-styles)=\n", "### Object Names \n", "\n", "Use the above-defined styles for naming Python items as follows: \n", "\n", "* Classes: `CamelCase` (`CapWords`) letters only such as `MyClass`\n", "* Constants: `UPPERCASE` letters only, where underscores may improve readability (e.g., use at a module level for example to assign water density `RHO = 1000`)\n", "* Exceptions: `CamelCase` (`CapWords`) letters only (exceptions should be predefined *Error* classes; typically use the suffix `Error` (e.g., `TypeError`)\n", "* Functions: `lowercase` letters only, where underscores may improve readability; sometimes `mixedCase` applies to ensure backward compatibility of prevailing styles\n", "* Methods (class function, non-public): `_lowercase` letters only with a leading underscore, where underscores may improve readability\n", "* Methods (class function, public): `lowercase` letters only, where underscores may improve readability\n", "* Modules: `lowercase` letters only, where underscores may improve readability\n", "* Packages: `lowercase` letters only, where underscores are discouraged\n", "* Variables: `lowercase` letters only, where underscores may improve readability\n", "* Variables (global): `lowercase` letters only, where underscores may improve readability; note that \"global\" should limit to variable usage within one module only." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{important}\n", "Never start a variable name with a number. Do **use `array_2d`**, but do **not use `2d_array`**.\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## More Code Style Recommendations\n", "\n", "To ensure code compatibility and program efficiency, the *PEP 8* style guide provides other general recommendations (read more in the [Python docs](https://www.python.org/dev/peps/pep-0008/#programming-recommendations)):\n", "\n", "* When defining a function, prefer `def` statements over `lambda` expressions, which are reasonable for one-time usage only\n", "* When exceptions are expected, use `try` - `except` clauses (see the {ref}`errors and exceptions ` section)\n", "* Ensure that methods and functions return objects consistently, for example:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def a_function_with_return(x):\n", " if x > 0:\n", " return np.sqrt(x)\n", " else:\n", " return np.nan" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Learning Success Check-up\n", "\n", "Take the [learning success test for this Jupyter notebook](https://forms.gle/jf2VrZVnJNFAJ5319).\n", "\n", "````{admonition} Unfold QR Code\n", ":class: tip, dropdown\n", "\n", "```{image} ../img/qr-codes/gle-pystyle.png\n", "```\n", "````" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.12" } }, "nbformat": 4, "nbformat_minor": 4 }