Introducing the Google Python Style Guide in Sourcery

A step by step guide towards implementing the Google Style Guide with Sourcery

Date

Sep 14, 2022

Google Style Guide

Not having a consistent style in your Python projects can lead to major headaches, especially when you’re working across a large team. Bringing in a standardized style guide or best practices can help cut out those pains - and Google’s Python Style Guide is one of the best pre-made style guides out there for Python projects.

We previously broke down the different elements of the Google Python Style Guide and talked about why they are useful rules to have in your Python projects. But today we want to take things a step further and actually help you add these rules to your code automatically with Sourcery.

To do this, we’re going to set up the Google Style Guide as a set of custom rules in Sourcery. If you’re not familiar with custom rules, here’s a quick tutorial to get started (but this article should also help you get up to speed pretty quickly). And if you aren’t using Sourcery you can get started in a few minutes by installing the Sourcery extension for VS Code, PyCharm, Vim, or other IDEs.

We’ve put together the full set of rules we go through here as a copyable list at the bottom of this article - feel free to grab the whole list to start applying them to your project or you can go through rule by rule to see which ones you like, which ones you don’t, and which ones you’d want to extend or tweak.


Within the Google Style Guide there are a few major groups of rules and guidelines:

We’ll take a look at each of these sets of rules and see how you can start having Sourcery automatically check for them. (Quick note - while we largely like the Google Python Style Guide as a series of best practices, we don’t agree with every rule they’ve laid out, but we’re presenting a full set to you so you can pick and choose which ones you think make sense for you.)

Import Rules

Standardizing how you use imports in your projects

No Wildcard Imports

Explicitly structures how packages and modules should be imported and restricts the use of wildcard imports

Rule:

  - id: no-wildcard-imports
    pattern: from ... import *
    description: Do not use wildcard imports
    explanation: |
      Use import statements for packages and modules only, not for individual classes or functions.
      - Use `import x` for importing packages and modules.
      - Use `from x import y` where `x` is the package prefix and `y` is the module name with no prefix.
      - Use `from x import y as z` if two modules named `y` are to be imported, if `y` conflicts with a top-level name defined in the current module, or if `y` is an inconveniently long name.
      - Use `import y as z` only when `z` is a standard abbreviation (e.g., np for numpy).
      From: Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)

Rules Syntax Tips & Tricks:

The ... syntax in a pattern looks to match anything in that piece of the pattern. More info on the ... syntax is available here.

Example

No Wildcard Imports.gif

No Relative Imports

To help make sure we don’t double import a package this rule explicitly restricts the use of relative imports in favor of absolute imports

Rule:

  - id: no-relative-imports
    description: Always use absolute imports instead of relative imports
    explanation: |
      Do not use relative names in imports. Even if the module is in the same package, use the full package name. This helps prevent unintentionally importing a package twice.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: from ${module} import ...
    condition: module.matches_regex(r"^\.")

Rules Syntax Tips & Tricks:

Conditions allow us to tailor when our rules do and don’t trigger. Here we set up the rule to only fire if the name of the module begins with a . Here’s a list of currently supported conditions

Example

No Relative Imports.gif

Using Standard Names for Aliases

There are a handful of packages that have typical aliases that are widely used that make our lives easier. By explicitly defining these aliases as acceptable shorthand, we can make sure we’re using the correct aliases that everyone agrees on and not introducing confusion.

Here are some of the most common aliases you’ll come across in Python, but you can easily add to this list if there are common aliases you’d want to use.

Rule:

  - id: use-standard-name-for-aliases-pandas
    description: Import `pandas` as `pd`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., pandas as ${alias}, ...
    condition: not alias.equals("pd")

  - id: use-standard-name-for-aliases-numpy
    description: Import `numpy` as `np`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., numpy as ${alias}, ...
    condition: not alias.equals("np")

  - id: use-standard-name-for-aliases-matplotlib-pyplot
    description: Import `matplotlib.pyplot` as `plt`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., matplotlib.pyplot as ${alias}, ...
    condition: not alias.equals("plt")

  - id: use-standard-name-for-aliases-tensorflow
    description: Import `tensorflow` as `tf`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., tensorflow as ${alias}, ...
    condition: not alias.equals("tf")

  - id: use-standard-name-for-aliases-datetime
    description: Import `datetime` as `dt`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., datetime as ${alias}, ...
    condition: not alias.equals("dt")

  - id: use-standard-name-for-aliases-tkinter
    description: Import `tkinter` as `tk`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., tkinter as ${alias}, ...
    condition: not alias.equals("tk")

  - id: use-standard-name-for-aliases-multiprocessing
    description: Import `multiprocessing` as `mp`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., multiprocessing as ${alias}, ...
    condition: not alias.equals("mp")

Note - We’re actively working to simplify the setup for rules like this where you want to define many cases of similar rules. We’ll update this section of this article as we make those changes.

Example

Use Standard Alias.gif


Naming Rules

Names can cause some of the biggest headaches in complex projects if naming conventions aren’t consistent. These rules help provide some standards to make names consistent and drive better readability

No Single Character Names

Single character names don’t give any information around what a variable or function is doing and can cause huge headaches for maintaining a section of code. There are a few exceptions (e.g. iterators) where a standard single-character name makes sense, but in general we should try to avoid them. Here we have two separate rules for avoiding variables and functions with single character names - which makes it easy to configure the minimum length of name for each one.

Rule:

  - id: avoid-single-character-names-variables
    pattern: ${var} = ${value}
    condition: var.character_count() == 1
    description: Avoid single character names
    explanation: |
      Avoid single character names, except for specifically allowed cases:
      - counters or iterators (e.g. `i`, `j`, `k`, `v`, et al.)
      - `e` as an exception identifier in `try`/`except` statements.
      - `f` as a file handle in `with` statements
      - private `TypeVar`s with no constraints (e.g. `_T`, `_U`, `_V`)
      From Google Style Guide [3.16.1](https://google.github.io/styleguide/pyguide.html#3161-names-to-avoid)

  - id: avoid-single-character-names-functions
    pattern: |
      def ${function_name}(...):
        ...
    condition: function_name.character_count() == 1
    description: Avoid single character names
    explanation: |
      Avoid single character names, except for specifically allowed cases:
      - counters or iterators (e.g. `i`, `j`, `k`, `v`, et al.)
      - `e` as an exception identifier in `try`/`except` statements.
      - `f` as a file handle in `with` statements
      - private `TypeVar`s with no constraints (e.g. `_T`, `_U`, `_V`)
      From Google Style Guide [3.16.1](https://google.github.io/styleguide/pyguide.html#3161-names-to-avoid)

Example

Avoid Single Character Names.gif

Avoid Type Suffixes

Adding in the type as a part of a variable name adds unnecessary complexity to the name which makes the code harder to understand. For built-in types, it should already be apparent what the type is without needing to state that as part of the variable name.

Rule:

  - id: name-type-suffix
    pattern: ${name} = ${value}
    condition: |
      name.ends_with("_dict")
      or name.ends_with("_list")
      or name.ends_with("_set")
      or name.ends_with("_int")
      or name.ends_with("_float")
      or name.ends_with("_str")
    description: Don't use the type of a variable as a suffix
    explanation: |
      Names shouldn't needlessly include the type of the variable.
      Such suffix might be OK for more complex types,
      e.g. first_account, advanced_account.
      But it's rarely necessary for built-in types.
      From Google Style Guide [3.16.1](https://google.github.io/styleguide/pyguide.html#s3.16-naming)

Rules Syntax Tips & Tricks:

You can set multiple conditions for the same rule with and and or

Example

Avoid Type Suffixes.gif

Error Named Error

For consistency, any type of exception we’re introducing needs to be named so that they end with Error.

Rule:

  - id: errors-named-error
    pattern: |
      class ${error}(${base}):
        ${statements*}
    condition: |
      (base.is_exception_type() or base.matches_regex("[A-Z][a-zA-Z]*Error")) and not error.matches_regex("[A-Z][a-zA-Z]*Error")
    description: Exception names must end in Error
    explanation: |
      From Google Style Guide [2.4.4](https://google.github.io/styleguide/pyguide.html#244-decision)

Example

Error Named Error.gif

Case Standards

In both the PEP 8 guidelines and the Google Python Style Guide there are recommendations that classes use camel case and variable declarations, argument names, and function names all use snake case.

Rule:

  - id: camel-case-classes
    pattern: |
      class ${class_name}(...):
        ...
    condition: not class_name.is_upper_camel_case()
    description: Use camel case for class names
    explanation: |
      Use camel case for class names
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#class-names)


  - id: snake-case-variable-declarations
    pattern: |
      ${var}: ${type_annotation}
    condition: not var.is_snake_case() and not var.in_module_scope()
    description: Use snake case for variable names
    explanation: |
      Use snake case for variables.
      This rule catches only variables that were declared with a type annotation.
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#function-and-variable-names)

  - id: snake-case-arguments
    pattern: |
      def ...(...,${arg_name}: ${type?} = ${default_value?},...):
        ...
    condition: not arg_name.is_snake_case() or arg_name.is_dunder_name()
    description: Use snake case for arguments
    explanation: |
      Use snake case for function and method arguments.
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#function-and-method-arguments)


  - id: snake-case-functions
    pattern: |
      def ${function_name}(...):
        ...
    condition: not function_name.is_snake_case()
    description: Use snake case for function names
    explanation: |
      Use snake case for function and method names.
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#function-and-variable-names)

Example

Case Rules.gif


Structural Rules

Within the Google Python Style Guide, there are a variety of different rules to help provide structural guidelines for how we write Python code. These are largely designed to help with consistency and readability.

No Static Methods

Instead of using static methods we should write module-level functions. The exception here is if we need to do so to integrate with an API in an existing library.

Rule:

  - id: do-not-use-staticmethod
    pattern: |
      @staticmethod
      def ${name}(...):
        ...
    description: Do not use the staticmethod decorator
    explanation: |
      Never use staticmethod unless forced to in order to integrate with an API defined in an existing library. Write a module level function instead.
      From: Google Style Guide [2.17.4](https://google.github.io/styleguide/pyguide.html#2174-decision)

Example

Do Not Use Static Method.gif

No Complex If Expressions

If Expressions can be great ways to simplify our Python code and make it more readable. But if they become too lengthy and any portion has to be split over multiple lines then they become significantly harder to understand and should be avoided.

Rule:

  - id: no-complex-if-expressions
    description: Only use conditional expressions for simple cases
    explanation: |
      Each portion [of the conditional expression] must fit on one line: `true-expression`, `if-expression`, `else-expression`. Use a complete if statement when things get more complicated.
      From Google Style Guide [2.11.4](https://google.github.io/styleguide/pyguide.html#2114-decision)
    pattern: ${value} if ${test} else ${default}
    condition: value.character_count() > 80 or test.character_count() > 80 or default.character_count()
      > 80

Example

No Complex If Expressions.gif

Lambdas Should Be Short

Like If Expressions, Lambdas can be an incredibly useful tool. But, like If Expressions, they also become unwieldy and hard to understand when they start spanning multiple lines. Only use Lambdas if they can exist on a single line.

  - id: lambdas-should-be-short
    description: Lambda functions should be kept to a single line
    explanation: |
      Okay to use them for one-liners.
      If the code inside the lambda function is longer than 60-80 chars, it’s probably better to define it as a regular nested function.
      From Google Style Guide [2.10.4](https://google.github.io/styleguide/pyguide.html#2104-decision)
    pattern: 'lambda ...: ${body}'
    condition: body.character_count() > 80

Example

Lambdas Should Be Short.gif

Generator Expressions instead of Lambdas Use generator expressions rather than using lambdas with filter() or map() to make expressions easier to read and debug.


Rule:

  - id: map-lambda-to-generator
    pattern: 'map(lambda ${arg}: ${expr}, ${items})'
    replacement: (${expr} for ${arg} in ${items})
    description: Replace mapping a lambda with a generator expression
    explanation: |
      Prefer generator expressions over map() or filter() with a lambda.
      From Google Style Guide [2.10](https://google.github.io/styleguide/pyguide.html#210-lambda-functions)

  - id: filter-lambda-to-generator
    pattern: 'filter(lambda ${arg}: ${expr}, ${items})'
    replacement: (${arg} for ${arg} in ${items} if ${expr})
    description: Replace filtering with a lambda with a generator expression
    explanation: |
      Prefer generator expressions over map() or filter() with a lambda.
      From Google Style Guide [2.10](https://google.github.io/styleguide/pyguide.html#210-lambda-functions)

Example

Generator Instead of Lambda.gif

Avoid Global Variables

Global variables have the potential to change module behaviour during the import and should be avoided.

Rule:

  - id: avoid-global-variables
    pattern: ${var} = ${value}
    condition: pattern.in_module_scope() and var.is_lower_case() and not var.starts_with("_")
      and var.is_identifier()
    description: Do not define variables at the module level - (found variable ${var})
    explanation: |
      Avoid global variables.
      If needed, global variables should be declared at the module level and made internal to the module by prepending an `_` to the name. External access to global variables must be done through public module-level functions.
      While module-level constants are technically variables, they are permitted and encouraged. For example: `MAX_HOLY_HANDGRENADE_COUNT = 3`. Constants must be named using all caps with underscores.
      From Google Style Guide [2.5.4](https://google.github.io/styleguide/pyguide.html#254-decision)

Example

Avoid Global Variables.gif

Avoid Trivial Properties

Getters and Setters are useful if they are being used to get or set a variable in a case where it’s complex or costly, but should not be used in simple cases such as simply reading or writing an internal attribute.

Rule:

  - id: avoid-trivial-properties
    description: Avoid defining trivial properties
    explanation: |
      Getter and setter functions (also called accessors and mutators) should be used when
      they provide a meaningful role or behavior for getting or setting a variable's
      value.
      In particular, they should be used when getting or setting the variable is complex
      or the cost is significant, either currently or in a reasonable future.
      If, for example, a pair of getters/setters simply read and write an internal
      attribute, the internal attribute should be made public instead. By comparison, if
      setting a variable means some state is invalidated or rebuilt, it should be a setter
      function. The function invocation hints that a potentially non-trivial operation is
      occurring. Alternatively, properties may be an option when simple logic is needed,
      or refactoring to no longer need getters and setters.
      From Google Style Guide [3.15](https://google.github.io/styleguide/pyguide#315-getters-and-setters)
    pattern: |
      @property
      def ${f}(self):
        return self.${value}
      ...
      @${f}.setter
      def ${setter}(self, ${other}):
        self.${value} = ${other}

Example

Avoid Trivial Properties.gif

No Long Functions

At Sourcery we’re big fans of trying to keep functions short to improve readability. Obviously, there are cases where we may need longer functions for one reason or another, but in general, the Google Python Style Guide recommends that functions remain shorter than 40 lines. If you want to tailor this rule to trigger for longer or shorter functions, just change the length limit.

Rule:

  - id: no-long-functions
    pattern: |
      def ${name}(...):
          ${statements+}
    condition: statements.statement_count() > 40
    description: Functions should be less than 40 lines
    explanation: |
      Prefer small and focused functions.
      We recognize that long functions are sometimes appropriate, so no hard limit is placed on function length. If a function exceeds about 40 lines, think about whether it can be broken up without harming the structure of the program.
      Even if your long function works perfectly now, someone modifying it in a few months may add new behavior. This could result in bugs that are hard to find. Keeping your functions short and simple makes it easier for other people to read and modify your code.
      From Google Style Guide [3.18](https://google.github.io/styleguide/pyguide.html#318-function-length)

Example

Functions Should be Short.gif

Do Not Use Has Key

In Python 2 the common syntax for checking if a key is in a dictionary was to use d.has_key(key), but in Python 3 the correct syntax is key in d

Rule:

  - id: do-not-use-has-key
    pattern: ${d}.has_key(${key})
    condition: d.has_type("dict")
    replacement: ${key} in ${d}
    description: Replace Python 2 syntax `dict.has_key` with a Python 3 membership
      test
    explanation: |
      Use default iterators and operators for types that support them, like lists, dictionaries, and files. The built-in types define iterator methods, too. Prefer these methods to methods that return lists, except that you should not mutate a container while iterating over it.
      From Google Style Guide [2.8.4](https://google.github.io/styleguide/pyguide.html#284-decision)
      Note that the method `dict.has_key` is only available in Python 2, but Sourcery only
      works with Python 3 code. Hence, this rule prevents programmers from erroneously
      checking membership with `dictionary.has_key(key)`, suggesting instead the use of
      the correct Python 3 syntax `key in dictionary`.

Rules Syntax Tips & Tricks:

The has_type condition allows you to set rules to only trigger (or not trigger) when a capture has a specific type.

Example

Do Not Use Has Keys.gif


Typing Rules

Adding in type annotations to our code helps to make it easier to read and debug in the future. These rules help define where we need type annotations. If you’re working on a project with a large section of legacy code without annotations, it might be useful to have these rules only for new code, otherwise you might be facing a bit of a headache trying to tackle all the existing missing annotations. You can set up inclusion or exclusion paths to easily define where you do and don't want these typing rules.

Here we have separate rules for parameter and return annotations. In general, we recommend using both, but you can easily tailor the rules to the style you want to adopt.

Rule:

  - id: require-parameter-annotation
    pattern: |
      def ${name}(..., ${arg}: !!!=${default?}, ...):
          ...
    condition: not name.starts_with("_") and not arg.equals("self") and not arg.equals("cls")
    paths:
      exclude:
        - test/
        - tests/
    description: Annotate parameter `${arg}` in public function/method `${name}` with
      a type annotation
    explanation: |
      Adding type annotations has several benefits:
      1. It improves the documentation of the function
      2. It allows the function to be checked for correctness
      3. It allows checking that the function callers are passing the correct params
      These [mypy docs](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#functions) describe how to
      annotate function arguments and return types.
      From Google Style Guide [2.21](https://google.github.io/styleguide/pyguide#221-type-annotated-code)

  - id: require-return-annotation
    pattern: |
      def ${name}(...) -> !!!:
          ...
    condition: not name.starts_with("_")
    paths:
      exclude:
        - test/
        - tests/
    description: Annotate public function/method `${name}` with a return type annotation
    explanation: |
      Adding type annotations has several benefits:
      1. It improves the documentation of the function
      2. It allows the function to be checked for correctness
      3. It allows checking that the function callers are passing the correct params
      These [mypy docs](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#functions) describe how to
      annotate function arguments and return types.
      From Google Style Guide [2.21](https://google.github.io/styleguide/pyguide#221-type-annotated-code)

Rules Syntax Tips & Tricks:

The !!! syntax is used in a pattern when we want to match a case where nothing matches the section of the pattern in question.

Example:

Typing Rules.gif


Docstring Rules

Docstrings are invaluable to help make complex projects more understandable and to help with collaboration. These rules help to check for docstrings in classes, modules, and functions. Similar to Type Annotations, these are rules you may only want to have applied to future code, otherwise you may see them flagging lots of legacy code.

Here we have three separate rules - one for each classes, modules, and functions. Note that in all 3 we’ve by default set up the rules so they don’t check for any code in the test/ or tests/ paths. You can easily add in other paths that you don’t want these rules to apply to.

A common tweak to docstrings-for-functions is to not require __dunder__ methods to have docstrings as well since their functionality is well-established in Python. If you would like to be lenient about those methods, replace the condition field of the rule docstrings-for-functions with:

not ((f.starts_with("_") and body.statement_count() < 5) or pattern.in_function_scope()
or f.is_dunder_name())
...

Rule:

  - id: docstrings-for-classes
    pattern: |
      class ${c}(...):
        """!!!"""
        ...
    description: Public classes should have docstrings
    explanation: |
      All public classes should have a docstring describing the class.
      The class docstring should also document the class' public attributes.
      From Google Style Guide [3.8.4](https://google.github.io/styleguide/pyguide.html#384-classes)
    condition: not c.starts_with("_")
    paths:
      exclude:
        - test/
        - tests/

  - id: docstrings-for-functions
    pattern: |
      def ${f}(...):
        """!!!"""
        ${body*}
    description: Functions should have docstrings
    explanation: |
      A function must have a docstring, unless it meets all of the following criteria:
      - not externally visible
      - very short
      - obvious
      From Google Style Guide [3.8.3](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods)
    condition: not ((f.starts_with("_") and body.statement_count() < 5) or pattern.in_function_scope())
    paths:
      exclude:
        - test/
        - tests/


  - id: docstrings-for-modules
    pattern: |
      '''!!!'''
      ...
    condition: pattern.in_module_scope()
    paths:
      exclude:
        - test/
        - tests/
    description: Modules should have docstrings
    explanation: |
      Modules (Python files) should start with docstrings describing the contents and usage of the module.
      From Google Style Guide [3.8.2](https://google.github.io/styleguide/pyguide.html#382-modules)

Rules Syntax Tips & Tricks:

Paths allow you to define sections of your codebase where you don’t want your rules to apply (exclude paths) or specifically where your rules should apply (include paths)

Examples:

Docstrings for Classes.gif

Docstrings for Functions.gif

Adding the Google Python Style Guide to your own project

To add all of these rules to your project and have Sourcery automatically check your code for them, just copy and paste the YAML below and save it as a .sourcery.yaml in your project directory. We’ve broken out the rules into the same categories we walked through below, so it should be easy to pick and choose which rules you want to apply and which you don’t care about.

The Google Style Guide is just the beginning of the ways you can customize Sourcery to check for different code standards, rules, and best practices. If there are any cases where you find your team consistently having to check for the same issues or make the same fixes and you want Sourcery to handle them for you, let me know and we can help you get everything set up.

rules:

#Import Rules
  - id: no-wildcard-imports
    pattern: from ... import *
    description: Do not use wildcard imports
    explanation: |
      Use import statements for packages and modules only, not for individual classes or functions.
      - Use `import x` for importing packages and modules.
      - Use `from x import y` where `x` is the package prefix and `y` is the module name with no prefix.
      - Use `from x import y as z` if two modules named `y` are to be imported, if `y` conflicts with a top-level name defined in the current module, or if `y` is an inconveniently long name.
      - Use `import y as z` only when `z` is a standard abbreviation (e.g., np for numpy).
      From: Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    tests:
      - match: from numpy import *
      - match: from pandas.series import *
      - match: from .something import *
      - no-match: from math import sin

  - id: no-relative-imports
    description: Always use absolute imports instead of relative imports
    explanation: |
      Do not use relative names in imports. Even if the module is in the same package, use the full package name. This helps prevent unintentionally importing a package twice.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: from ${module} import ...
    condition: module.matches_regex(r"^\.")
    tests:
      - match: from . import important_function
      - match: from .my_module import important_function
      - match: from ..my_module import important_function, unimportant_function
      - no-match: from my_company.my_module import important_function
      - no-match: from pathlib import Path

  - id: use-standard-name-for-aliases-pandas
    description: Import `pandas` as `pd`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., pandas as ${alias}, ...
    condition: not alias.equals("pd")
    tests:
      - no-match: import pandas
      - no-match: from pandas import DataFrame
      - no-match: import pandas as pd
      - no-match: import numpy, pandas as pd, tensorflow
      - no-match: from modin import pandas as pds
      - no-match: import modin.pandas as pds
      - match: import pandas as pds
      - match: import pandas as np
      - match: import numpy, pandas as pds, tensorflow

  - id: use-standard-name-for-aliases-numpy
    description: Import `numpy` as `np`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., numpy as ${alias}, ...
    condition: not alias.equals("np")
    tests:
      - no-match: import numpy
      - no-match: from numpy import ndarray
      - no-match: import numpy as np
      - no-match: import pandas, numpy as np, tensorflow
      - match: import numpy as numpie
      - match: import numpy as pd
      - match: import pandas, numpy as numpie, tensorflow

  - id: use-standard-name-for-aliases-matplotlib-pyplot
    description: Import `matplotlib.pyplot` as `plt`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., matplotlib.pyplot as ${alias}, ...
    condition: not alias.equals("plt")
    tests:
      - no-match: import matplotlib.pyplot
      - no-match: from matplotlib.pyplot import Figure
      - no-match: import matplotlib.pyplot as plt
      - no-match: import pandas, matplotlib.pyplot as plt, tensorflow
      - match: import matplotlib.pyplot as mplplot
      - match: import pandas, matplotlib.pyplot as mplplot, tensorflow

  - id: use-standard-name-for-aliases-tensorflow
    description: Import `tensorflow` as `tf`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., tensorflow as ${alias}, ...
    condition: not alias.equals("tf")
    tests:
      - no-match: import tensorflow
      - no-match: from tensorflow import keras
      - no-match: import tensorflow as tf
      - no-match: import pandas, tensorflow as tf
      - match: import tensorflow as tflow
      - match: import pandas, tensorflow as tflow

  - id: use-standard-name-for-aliases-datetime
    description: Import `datetime` as `dt`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., datetime as ${alias}, ...
    condition: not alias.equals("dt")
    tests:
      - no-match: import datetime
      - no-match: from datetime import datetime
      - no-match: import datetime as dt
      - no-match: import pandas, datetime as dt
      - match: import datetime as dtime
      - match: import pandas, datetime as dtime, tensorflow

  - id: use-standard-name-for-aliases-tkinter
    description: Import `tkinter` as `tk`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., tkinter as ${alias}, ...
    condition: not alias.equals("tk")
    tests:
      - no-match: import tkinter
      - no-match: from tkinter import ttk
      - no-match: import tkinter as tk
      - no-match: import pandas, tkinter as tk
      - match: import tkinter as t
      - match: import pandas, tkinter as t, tensorflow

  - id: use-standard-name-for-aliases-multiprocessing
    description: Import `multiprocessing` as `mp`
    explanation: |
      Use `import y as z` only when `z` is a standard abbreviation.
      From Google Style Guide [2.2.4](https://google.github.io/styleguide/pyguide.html#224-decision)
    pattern: import ..., multiprocessing as ${alias}, ...
    condition: not alias.equals("mp")
    tests:
      - no-match: import multiprocessing
      - no-match: from multiprocessing import Pool
      - no-match: import multiprocessing as mp
      - no-match: import pandas, multiprocessing as mp
      - match: import multiprocessing as multi
      - match: import pandas, multiprocessing as multi, tensorflow

# Naming Rules

  - id: avoid-single-character-names-variables
    pattern: ${var} = ${value}
    condition: var.character_count() == 1
    description: Avoid single character names
    explanation: |
      Avoid single character names, except for specifically allowed cases:
      - counters or iterators (e.g. `i`, `j`, `k`, `v`, et al.)
      - `e` as an exception identifier in `try`/`except` statements.
      - `f` as a file handle in `with` statements
      - private `TypeVar`s with no constraints (e.g. `_T`, `_U`, `_V`)
      From Google Style Guide [3.16.1](https://google.github.io/styleguide/pyguide.html#3161-names-to-avoid)
    tests:
      - match: i = 0
      - no-match: initial_value = 0
      - match: A = get_account()
      - no-match: account = get_account()
      - no-match: |
          for i in range(10):
              print(i)
      - no-match: |
          try:
              explode_computer()
          except ExplosionError as e:
              pass
      - no-match: |
          with open("file.txt") as f:
              contents = f.read()
      - no-match: |
          from typing import TypeVar
          _T = TypeVar("_T")

  - id: avoid-single-character-names-functions
    pattern: |
      def ${function_name}(...):
        ...
    condition: function_name.character_count() == 1
    description: Avoid single character names
    explanation: |
      Avoid single character names, except for specifically allowed cases:
      - counters or iterators (e.g. `i`, `j`, `k`, `v`, et al.)
      - `e` as an exception identifier in `try`/`except` statements.
      - `f` as a file handle in `with` statements
      - private `TypeVar`s with no constraints (e.g. `_T`, `_U`, `_V`)
      From Google Style Guide [3.16.1](https://google.github.io/styleguide/pyguide.html#3161-names-to-avoid)
    tests:
      - match: |
          def f():
              pass
      - match: |
          async def f():
              pass
      - no-match: |
          def function_with_good_and_descriptive_name():
              pass
      - match: |
          def g(a, b: int = 0, **kwargs) -> str:
              if b > 5:
                return str(a) + str(b) + str(kwargs)
              return "b <= 4"
      - no-match: |
          def perform_calculation(a, b: int = 0, **kwargs) -> str:
              if b > 5:
                return str(a) + str(b) + str(kwargs)
              return "b <= 4"
      - match: |
          class MyClass:
            def m(self, a: int, b: float) -> str:
              print(a)
              return repr(b)
      - no-match: |
          class MyClass:
            def good_method(self, a: int, b: float) -> str:
              print(a)
              return repr(b)

  - id: name-type-suffix
    pattern: ${name} = ${value}
    condition: |
      name.ends_with("_dict")
      or name.ends_with("_list")
      or name.ends_with("_set")
      or name.ends_with("_int")
      or name.ends_with("_float")
      or name.ends_with("_str")
    description: Don't use the type of a variable as a suffix
    explanation: |
      Names shouldn't needlessly include the type of the variable.
      Such suffix might be OK for more complex types,
      e.g. first_account, advanced_account.
      But it's rarely necessary for built-in types.
      From Google Style Guide [3.16.1](https://google.github.io/styleguide/pyguide.html#s3.16-naming)
    tests:
      - match: magic_int = 42
      - match: magic_int = 42.00
      - match: magic_float = 42
      - match: magic_float = 42.00
      - match: custom_notes_dict = {}

  - id: errors-named-error
    pattern: |
      class ${error}(${base}):
        ${statements*}
    condition: (base.is_exception_type() or base.matches_regex("[A-Z][a-zA-Z]*Error"))
      and not error.matches_regex("[A-Z][a-zA-Z]*Error")
    description: Exception names must end in Error
    explanation: |
      From Google Style Guide [2.4.4](https://google.github.io/styleguide/pyguide.html#244-decision)
    tests:
      - match: |
          class Foo(ValueError):
            ...
      - match: |
          class ExampleException(CustomError):
            def __init__(self, msg):
              ...
      - match: |
          class InvalidName(Exception):
            ...
      - match: |
          class InvalidName(BaseException):
            ...
      - no-match: |
          class MyError(Exception):
            def __init__(self, msg):
              ...
      - no-match: |
          class NameError(AttributeError):
            ...
      - no-match: |
          class Dog(Mammal):
            ...

  - id: snake-case-variable-declarations
    pattern: |
      ${var}: ${type_annotation}
    condition: not var.is_snake_case() and not var.in_module_scope()
    description: Use snake case for variable names
    explanation: |
      Use snake case for variables.
      This rule catches only variables that were declared with a type annotation.
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#function-and-variable-names)
    tests:
      - match: |
          def some_function():
            miXed: int
      - match: |
          def some_function():
            CamelCase: int
      - match: |
          def some_function():
            CamelCase42: int
      - match: |
          def some_function():
            mixed_and_underScore: int
      - match: |
          def some_function():
            too__many__underscores: str
      - match: |
          def some_function():
            _too__many__underscores: str
      - match: |
          def some_function():
            ___3_underscores_prefix: str
      - match: |
          def some_function():
            double_underscore_suffix__: str
      - no-match: |
          def some_function():
            nr: int = 42
      - no-match: |
          def some_function():
            miXed: int = 42
      - no-match: |
          def some_function():
            snake_nr: int = 42
      - no-match: simple_NR = 42
      - no-match: CONTEXT = "whatever"
      - no-match: |
          def some_function():
            _initial_value: int = 0
      - no-match: |
          def some_function():
            __initial_value: int = 0
      - no-match: __version__ = "3.14"

  - id: snake-case-arguments
    pattern: |
      def ...(...,${arg_name}: ${type?} = ${default_value?},...):
        ...
    condition: not arg_name.is_snake_case() or arg_name.is_dunder_name()
    description: Use snake case for arguments
    explanation: |
      Use snake case for function and method arguments.
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#function-and-method-arguments)
    tests:
      - match: |
          def placeholder(miXed):
            pass
      - match: |
          def placeholder(randomWord):
            pass
      - match: |
          def placeholder(randomWord: str):
            pass
      - match: |
          def placeholder(randomWord: str = "random"):
            pass
      - match: |
          def placeholder(randomWord, other):
            pass
      - match: |
          class Something:
            def placeholder(self,myCamelWord):
              pass
      - match: |
          def placeholder(too__many__underscores):
            pass
      - match: |
          def placeholder(double_underscore_suffix__):
            pass
      - match: |
          def placeholder(mixed_and_underScore):
            pass
      - match: |
          def placeholder(__dunder_arg__):
            pass
      - no-match: |
          def placeholder(nice_arg_name):
            pass
      - no-match: |
          def placeholder(nice_arg_name: str):
            pass
      - no-match: |
          def placeholder(nice_arg_name: str = "random"):
            pass
      - no-match: |
          def placeholder(simple):
            pass
      - no-match: |
          def placeholder(simple: bool = False):
            pass
      - no-match: |
          def placeholder(nr, other_nr, sth_completely_different):
            pass
      - no-match: |
          class Something:
            def placeholder(self,simple: bool = False):
              pass

  - id: snake-case-functions
    pattern: |
      def ${function_name}(...):
        ...
    condition: not function_name.is_snake_case()
    description: Use snake case for function names
    explanation: |
      Use snake case for function and method names.
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#function-and-variable-names)
    tests:
      - match: |
          def miXed():
            pass
      - match: |
          def CamelCase():
            pass
      - match: |
          class Something:
            def CamelCase(self):
              pass
      - match: |
          def too__many__underscores():
            pass
      - match: |
          def double_underscore_suffix__():
            pass
      - match: |
          def mixed_and_underScore():
            pass
      - no-match: |
          def nice_function_name():
            pass
      - no-match: |
          def _private():
            pass
      - no-match: |
          def __very_private():
            pass
      - no-match: |
          class Something:
            def single(self):
              pass
      - no-match: |
          class Something:
            def __init__(self):
              pass

  - id: camel-case-classes
    pattern: |
      class ${class_name}(...):
        ...
    condition: not class_name.is_upper_camel_case()
    description: Use camel case for class names
    explanation: |
      Use camel case for class names
      From Google Style Guide [3.16.2](https://google.github.io/styleguide/pyguide.html#s3.16.2-naming-conventions) and [PEP 8](https://peps.python.org/pep-0008/#class-names)
    tests:
      - match: |
          class lower:
            pass
      - match: |
          class UPPER:
            pass
      - match: |
          class snake_case:
            pass
      - match: |
          class UPPER_UNDERSCORE:
            pass
      - match: |
          class CamelCase_WithUnderscore:
            pass
      - no-match: |
          class CamelCase:
            pass
      - no-match: |
          class CamelCase42:
            pass
      - no-match: |
          class B:
            pass

# Structural Rules

  - id: do-not-use-staticmethod
    pattern: |
      @staticmethod
      def ${name}(...):
        ...
    description: Do not use the staticmethod decorator
    explanation: |
      Never use staticmethod unless forced to in order to integrate with an API defined in an existing library. Write a module level function instead.
      From: Google Style Guide [2.17.4](https://google.github.io/styleguide/pyguide.html#2174-decision)
    tests:
      - match: |
          @staticmethod
          def suggested_event(new_suggestion) -> str:
              pass
      - match: |
          @staticmethod
          def suggested_event(new_suggestion: bool) -> str:
              pass
      - no-match: |
          def suggested_event(new_suggestion) -> str:
              pass
      - no-match: |
          @staticmethod # Note that this will not currently trigger where there are other decorators
          @other_decorator
          def suggested_event(new_suggestion) -> str:
              pass

  - id: no-complex-if-expressions
    description: Only use conditional expressions for simple cases
    explanation: |
      Each portion [of the conditional expression] must fit on one line: `true-expression`, `if-expression`, `else-expression`. Use a complete if statement when things get more complicated.
      From Google Style Guide [2.11.4](https://google.github.io/styleguide/pyguide.html#2114-decision)
    pattern: ${value} if ${test} else ${default}
    condition: value.character_count() > 80 or test.character_count() > 80 or default.character_count()
      > 80
    tests:
      - no-match: a = 1 if cond else 2
      - match: a = 1 if this_is_an_incredibly_long_condition_that_is_more_than_80_characters_long_no_joking_around
          else 2
      - match: a = this_is_an_incredibly_long_value_that_is_more_than_80_characters_long_no_joking_around
          if cond else 2
      - match: a = 1 if cond else this_is_an_incredibly_long_value_that_is_more_than_80_characters_long_no_joking_around
      - match: a = 1 if cond else this_is_an_incredibly_long_value + is_more_than_80_characters_long_no_joking_around()

  - id: lambdas-should-be-short
    description: Lambda functions should be kept to a single line
    explanation: |
      Okay to use them for one-liners.
      If the code inside the lambda function is longer than 60-80 chars, it’s probably better to define it as a regular nested function.
      From Google Style Guide [2.10.4](https://google.github.io/styleguide/pyguide.html#2104-decision)
    pattern: 'lambda ...: ${body}'
    condition: body.character_count() > 80
    tests:
      - no-match: 'lambda x: x**2'
      - no-match: 'lambda x, y: x**y'
      - match: 'lambda x: do_something_very_long_and_involved_with(x) - do_other_very_long_and_involved_things_with(x)'
      - match: 'lambda x, y, z: do_something_very_long_and_involved_with(x) - do_other_very_long_and_involved_things_with(y,
          z)'

  - id: map-lambda-to-generator
    pattern: 'map(lambda ${arg}: ${expr}, ${items})'
    replacement: (${expr} for ${arg} in ${items})
    description: Replace mapping a lambda with a generator expression
    explanation: |
      Prefer generator expressions over map() or filter() with a lambda.
      From Google Style Guide [2.10](https://google.github.io/styleguide/pyguide.html#210-lambda-functions)
    tests:
      - match: 'transformed_things = map(lambda x: x**2, things)'
        expect: transformed_things = (x**2 for x in things)
      - match: 'list(map(lambda x: x**2, things))'
        expect: list(x**2 for x in things)
      - no-match: 'filter(lambda x: x > x**2, things)'

  - id: filter-lambda-to-generator
    pattern: 'filter(lambda ${arg}: ${expr}, ${items})'
    replacement: (${arg} for ${arg} in ${items} if ${expr})
    description: Replace filtering with a lambda with a generator expression
    explanation: |
      Prefer generator expressions over map() or filter() with a lambda.
      From Google Style Guide [2.10](https://google.github.io/styleguide/pyguide.html#210-lambda-functions)
    tests:
      - match: 'filtered_things = filter(lambda x: x > x**2, things)'
        expect: filtered_things = (x for x in things if x > x**2)
      - match: 'list(filter(lambda x: x > x**2, things))'
        expect: list(x for x in things if x > x**2)
      - no-match: 'map(lambda x: x**2, things)'

  - id: avoid-global-variables
    pattern: ${var} = ${value}
    condition: pattern.in_module_scope() and var.is_lower_case() and not var.starts_with("_")
      and var.is_identifier()
    description: Do not define variables at the module level - (found variable ${var})
    explanation: |
      Avoid global variables.
      If needed, global variables should be declared at the module level and made internal to the module by prepending an `_` to the name. External access to global variables must be done through public module-level functions.
      While module-level constants are technically variables, they are permitted and encouraged. For example: `MAX_HOLY_HANDGRENADE_COUNT = 3`. Constants must be named using all caps with underscores.
      From Google Style Guide [2.5.4](https://google.github.io/styleguide/pyguide.html#254-decision)
    tests:
      - match: max_holy_handgrenade_count = 3
      - match: 'max_holy_handgrenade_count: int = 3'
      - no-match: holy_handgrenade[1] = 3
      - no-match: _max_holy_handgrenade_count = 3
      - no-match: HolyGrenades = Dict[str, Grenade]
      - no-match: MAX_HOLY_HANDGRENADE_COUNT = 3
      - no-match: |
          def f():
              max_holy_handgrenade_count = 3

  - id: avoid-trivial-properties
    description: Avoid defining trivial properties
    explanation: |
      Getter and setter functions (also called accessors and mutators) should be used when
      they provide a meaningful role or behavior for getting or setting a variable's
      value.
      In particular, they should be used when getting or setting the variable is complex
      or the cost is significant, either currently or in a reasonable future.
      If, for example, a pair of getters/setters simply read and write an internal
      attribute, the internal attribute should be made public instead. By comparison, if
      setting a variable means some state is invalidated or rebuilt, it should be a setter
      function. The function invocation hints that a potentially non-trivial operation is
      occurring. Alternatively, properties may be an option when simple logic is needed,
      or refactoring to no longer need getters and setters.
      From Google Style Guide [3.15](https://google.github.io/styleguide/pyguide#315-getters-and-setters)
    pattern: |
      @property
      def ${f}(self):
        return self.${value}
      ...
      @${f}.setter
      def ${setter}(self, ${other}):
        self.${value} = ${other}
    tests:
      - match: |
          class Student:
            def __init__(self, name):
              self._name = name
            @property
            def name(self):
              return self._name
            @name.setter
            def name(self, new_name):
              self._name = new_name
      - match: |
          class Student:
            def __init__(self, name, grade):
              self._name = name
              self._grade = grade
            @property
            def name(self):
              return self._name
            def get_grade(self):
              return self._grade
            @name.setter
            def name(self, new_name):
              self._name = new_name
      - match: |
          class Student:
            def __init__(self, name: str) -> None:
              self._name: str = name
            @property
            def name(self) -> str:
              return self._name
            @name.setter
            def name(self, new_name: str) -> None:
              self._name = new_name
      - no-match: |
          class Student:
            def __init__(self, name: str) -> None:
              self._name: str = name
            @property
            def name(self) -> str:
              return self._name.title() # perform computation on name
            @name.setter
            def name(self, new_name: str) -> None:
              self._name = new_name
      - no-match: |
          class Student:
            def __init__(self, name: str) -> None:
              self._name: str = name
            @property
            def name(self) -> str:
              return self._name.title() # perform computation on name
            # no setter: this means that `Student.name` is read-only

  - id: no-long-functions
    pattern: |
      def ${name}(...):
          ${statements+}
    condition: statements.statement_count() > 40
    description: Functions should be less than 40 lines
    explanation: |
      Prefer small and focused functions.
      We recognize that long functions are sometimes appropriate, so no hard limit is placed on function length. If a function exceeds about 40 lines, think about whether it can be broken up without harming the structure of the program.
      Even if your long function works perfectly now, someone modifying it in a few months may add new behavior. This could result in bugs that are hard to find. Keeping your functions short and simple makes it easier for other people to read and modify your code.
      From Google Style Guide [3.18](https://google.github.io/styleguide/pyguide.html#318-function-length)
    tests:
      - no-match: |
          def f(a, b) -> int:
              x()

  - id: do-not-use-has-key
    pattern: ${d}.has_key(${key})
    condition: d.has_type("dict")
    replacement: ${key} in ${d}
    description: Replace Python 2 syntax `dict.has_key` with a Python 3 membership
      test
    explanation: |
      Use default iterators and operators for types that support them, like lists, dictionaries, and files. The built-in types define iterator methods, too. Prefer these methods to methods that return lists, except that you should not mutate a container while iterating over it.
      From Google Style Guide [2.8.4](https://google.github.io/styleguide/pyguide.html#284-decision)
      Note that the method `dict.has_key` is only available in Python 2, but Sourcery only
      works with Python 3 code. Hence, this rule prevents programmers from erroneously
      checking membership with `dictionary.has_key(key)`, suggesting instead the use of
      the correct Python 3 syntax `key in dictionary`.
    tests:
      - match: |
          tasks_for_today: dict[str, Task] = get_tasks("today")
          is_week_day = tasks_for_today.has_key("work")
        expect: |
          tasks_for_today: dict[str, Task] = get_tasks("today")
          is_week_day = "work" in tasks_for_today
      - match: |
          def is_movie_in_database(movie: Movie, movies: Dict[Movie, MovieSpec]) -> bool:
            return movies.has_key(movie)
        expect: |
          def is_movie_in_database(movie: Movie, movies: Dict[Movie, MovieSpec]) -> bool:
            return movie in movies
      - match: |
          pro_users = {
            user.name: user
            for user in database.fetch(User)
          }
          if pro_users.has_key(get_current_user().name):
            print("You've signed in under the PRO subscription")
          else:
            print("Please sign up for PRO to use this awesome feature!")
        expect: |
          pro_users = {
            user.name: user
            for user in database.fetch(User)
          }
          if get_current_user().name in pro_users:
            print("You've signed in under the PRO subscription")
          else:
            print("Please sign up for PRO to use this awesome feature!")
      - no-match: |
          # Not sure of `random_object` is a dictionary
          random_object.has_key(random_key)
      - no-match: |
          # Custom type `Database` is not a dictionary
          database: Database = get_database()
          print(database.has_key("dinosaur"))

# Typing Rules

  - id: require-parameter-annotation
    pattern: |
      def ${name}(..., ${arg}: !!!=${default?}, ...):
          ...
    condition: not name.starts_with("_") and not arg.equals("self") and not arg.equals("cls")
    paths:
      exclude:
        - test/
        - tests/
    description: Annotate parameter `${arg}` in public function/method `${name}` with
      a type annotation
    explanation: |
      Adding type annotations has several benefits:
      1. It improves the documentation of the function
      2. It allows the function to be checked for correctness
      3. It allows checking that the function callers are passing the correct params
      These [mypy docs](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#functions) describe how to
      annotate function arguments and return types.
      From Google Style Guide [2.21](https://google.github.io/styleguide/pyguide#221-type-annotated-code)
    tests:
      - match: |
          def add(a, b: int):
            return a + b
      - match: |
          def f(a=1):
            return a * 2
      - no-match: |
          def f() -> int:
              pass
      - no-match: |
          def f(a: int, b: str):
              pass
      - no-match: |
          def f(self, a: int, b: str):
              pass
      - no-match: |
          def f(cls, a: int, b: str):
              pass

  - id: require-return-annotation
    pattern: |
      def ${name}(...) -> !!!:
          ...
    condition: not name.starts_with("_")
    paths:
      exclude:
        - test/
        - tests/
    description: Annotate public function/method `${name}` with a return type annotation
    explanation: |
      Adding type annotations has several benefits:
      1. It improves the documentation of the function
      2. It allows the function to be checked for correctness
      3. It allows checking that the function callers are passing the correct params
      These [mypy docs](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#functions) describe how to
      annotate function arguments and return types.
      From Google Style Guide [2.21](https://google.github.io/styleguide/pyguide#221-type-annotated-code)
    tests:
      - match: |
          def f():
              x()
      - no-match: |
          def f() -> int:
              x()
      - match: |
          def f(a: int, b: str):
              x()
      - no-match: |
          def f(something) -> int:
              x()

# Docstring Rules

  - id: docstrings-for-classes
    pattern: |
      class ${c}(...):
        """!!!"""
        ...
    description: Public classes should have docstrings
    explanation: |
      All public classes should have a docstring describing the class.
      The class docstring should also document the class' public attributes.
      From Google Style Guide [3.8.4](https://google.github.io/styleguide/pyguide.html#384-classes)
    condition: not c.starts_with("_")
    paths:
      exclude:
        - test/
        - tests/
    tests:
      - match: |
          class CheeseShopAddress:
            ...
      - match: |
          class CheeseShopAddress(Address):
            ...
      - match: |
          class OutOfCheeseError(Exception):
            def __str__(self):
              ...
      - no-match: |
          class CheeseShopAddress:
            """The address of a cheese shop."""
      - no-match: |
          class OutOfCheeseError(Exception):
            """No more cheese is available."""
            def __str__(self):
              ...
      - no-match: |
          class _BrieCounter:
            limit: 500

  - id: docstrings-for-functions
    pattern: |
      def ${f}(...):
        """!!!"""
        ${body*}
    description: Functions should have docstrings
    explanation: |
      A function must have a docstring, unless it meets all of the following criteria:
      - not externally visible
      - very short
      - obvious
      From Google Style Guide [3.8.3](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods)
    condition: not ((f.starts_with("_") and body.statement_count() < 5) or pattern.in_function_scope())
    paths:
      exclude:
        - test/
        - tests/
    tests:
      - match: |
          def grow(plant):
            assert plant.is_alive()
            _grow(plant)
      - match: |
          def _grow(plant):
            pot = plant.owner.get_empty_pot()
            pot.contents.add("soil", fraction=0.5)
            pot.contents.add(plant, orientation="vertical")
            for day in range(0, plant.__class__.gestation_time, plant.__class__.watering_interval):
              pot.contents.add("water", amount="200ml")
      - no-match: |
          class PotContents:
            def add(item, fraction=None, amount=None, **kwargs):
              """Adds `item` to the pot.
              The pot can be filled to either a `fraction` of its volume, or by an `amount` specified.
              """
              ...
      - no-match: |
          def _log_growth(plant):
            logger.info("plant size: %s", plant.size)
      - no-match: |
          def log_growth_error(f):
            """Captures and logs any growth errors inside `f`"""
            def wrapped(*args, **kwargs):
              try:
                return f(*args, **kwargs)
              except GrowthError:
                logger.warning("growth error")
            return wrapped

  - id: docstrings-for-modules
    pattern: |
      '''!!!'''
      ...
    condition: pattern.in_module_scope()
    paths:
      exclude:
        - test/
        - tests/
    description: Modules should have docstrings
    explanation: |
      Modules (Python files) should start with docstrings describing the contents and usage of the module.
      From Google Style Guide [3.8.2](https://google.github.io/styleguide/pyguide.html#382-modules)
    tests:
      - match: |
          def hello():
            print("hello")
      - match: |
          class Hello:
            """Hello"""
            def hello():
              """Prints 'hello'"""
              print("hello")
      - no-match: |
          """Hello module"""
          def hello():
            print("hello")