Skip to main content
Version: Next

Coding Styles and Rules

To maintain consistency and quality across the codebase, we follow these coding standards and rules. All contributors are expected to adhere to these guidelines.

pre-commit enabled Black code style isort imports Flake8 linting MyPy type checking Pylint linting

Python Code Style​

PEP 8​

We follow PEP 8 for Python code style with a few modifications:

  • Maximum line length: 100 characters
  • Use 4 spaces for indentation (never tabs)
  • Use snake_case for functions, methods, and variables
  • Use CamelCase for classes
  • Use UPPER_CASE for constants

Type Annotations​

We use type annotations throughout the codebase to improve code readability and enable static type checking:

def get_task(task_id: str) -> SampleDataModel:
"""
Retrieve a task by its ID.

Args:
task_id: The ID of the task to retrieve

Returns:
ClickUpTask: The retrieved task object

Raises:
TaskNotFoundError: If the task doesn't exist
"""
# Implementation

Type annotations are checked using MyPy during CI/CD.

Python Typing Standards​

We adhere to modern Python typing standards as defined in various PEPs:

PEP 484 - Type Hints​

PEP 484 introduced the typing module and the core syntax for type annotations:

from typing import Dict, List, Optional, Union

# Use Optional for values that may be None
def get_user(user_id: Optional[str] = None) -> Optional[User]:
pass

# Use Union for values that could be multiple types
def process_input(data: Union[str, bytes]) -> Dict[str, int]:
pass

PEP 585 - Type Hinting Generics in Standard Collections​

PEP 585 allows using built-in collection types directly for annotations (Python 3.9+):

# Python 3.9+ preferred style
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}

# Instead of the older style with the typing module
# def process_items(items: List[str]) -> Dict[str, int]:

When supporting Python versions before 3.9, use the typing module equivalents.

PEP 646 - Variadic Generics​

PEP 646 introduced TypeVarTuple for variadic generics (Python 3.11+):

from typing import TypeVar, TypeVarTuple, Unpack

T = TypeVar('T')
Ts = TypeVarTuple('Ts')

def process_batch(batch: tuple[T, *Unpack[Ts]]) -> list[T]:
first, *rest = batch
return [first]

Use this for handling arbitrary homogeneous sequences when needed in higher-order functions.

PEP 695 - Type Parameter Syntax​

PEP 695 introduced simplified syntax for generic type definitions (Python 3.12+):

# Python 3.12+ simplified generic syntax
def identity[T](x: T) -> T:
return x

# Instead of the more verbose
# T = TypeVar('T')
# def identity(x: T) -> T:
# return x

For backward compatibility, use the TypeVar approach when targeting Python versions before 3.12.

Duck Typing and Protocols​

Following Python's "duck typing" philosophy while maintaining type safety, we use Protocols (PEP 544) instead of requiring specific classes:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Clickable(Protocol):
def click(self) -> None:
...

def perform_click(item: Clickable) -> None:
item.click() # Works with any object that has a click() method

Protocol Best Practices​

  1. Use structural subtyping: Focus on what objects do (methods/attributes they provide) rather than what they are (their class hierarchy)

  2. Define minimal interfaces: Only include methods and attributes that are actually needed

  3. Use @runtime_checkable sparingly: Only when you need isinstance() checks, which should be rare

  4. Combine with generics when appropriate:

from typing import TypeVar, Protocol, Generic

T = TypeVar('T')

class Repository(Protocol, Generic[T]):
def get(self, id: str) -> T:
...
def save(self, item: T) -> None:
...
  1. Use TypedDict for dictionary interfaces:
from typing import TypedDict

class TaskData(TypedDict):
id: str
name: str
status: str
priority: int

Type Narrowing​

Use type guards and assertion functions for type narrowing:

from typing import TypeGuard, assert_type

def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)

def process_strings(items: list[object]) -> None:
if is_string_list(items):
# items is now known to be list[str]
for item in items:
assert_type(item, str)
print(item.upper())

Docstrings​

We use Google-style docstrings:

def function_name(param1: type, param2: type) -> return_type:
"""Short description of the function.

Longer description if necessary, explaining details.

Args:
param1: Description of param1
param2: Description of param2

Returns:
Description of the return value

Raises:
ExceptionType: When and why this exception is raised
"""
# Implementation

Code Organization​

Imports​

Organize imports in the following order, with a blank line between each group:

  1. Standard library imports
  2. Third-party library imports
  3. Local application imports
import os
import sys
from typing import Dict, List, Optional

import httpx
from pydantic import BaseModel, Field

from your_repo.models import SampleDataModel
from your_repo.utils import logger

File and Directory Structure​

  • One class per file where appropriate
  • Group related functionality into modules
  • Keep files focused on a single responsibility

Code Quality Tools​

We use several tools to ensure code quality:

Pre-commit Hooks​

We use pre-commit hooks to automatically check code quality before committing. Install them with:

pre-commit install

Our pre-commit hooks include:

  • Black for code formatting
  • isort for import sorting
  • flake8 for linting
  • mypy for type checking

PyLint Configuration​

The project uses PyLint for comprehensive code analysis with configuration in .pylintrc. Our PyLint rules are based on the Google Python Style Guide.

Key PyLint settings include:

[MASTER]
ignore=CVS
ignore-patterns=
persistent=yes
load-plugins=
jobs=0

[MESSAGES CONTROL]
# Disable specific PyLint checks that conflict with our style choices
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead

Run PyLint on your code with:

pylint your_module_or_file.py

MyPy Configuration​

MyPy type checking is configured in mypy.ini, enforcing strict typing rules:

[mypy]
python_version = 3.13
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
strict_optional = True

# Exclude scripts directory from type checking
[mypy-scripts.*]
ignore_errors = True

Run MyPy to check your code with:

mypy your_module_or_package

Black Configuration​

Black is configured in pyproject.toml:

[tool.black]
line-length = 100
target-version = ['py39']

Flake8 Configuration​

Flake8 is configured in .flake8:

[flake8]
max-line-length = 100
exclude = .git,__pycache__,build,dist

Testing​

  • All code should have unit tests
  • Aim for high test coverage, especially for critical paths
  • Tests should be fast and independent of each other

Pull Request Guidelines​

  • Keep changes focused and limited in scope
  • Include tests for new functionality
  • Update documentation as needed
  • Run pre-commit hooks before submitting
  • Address all CI/CD failures

Best Practices​

  • Write self-documenting code with descriptive variable and function names
  • Follow the DRY (Don't Repeat Yourself) principle
  • Keep functions small and focused on a single responsibility
  • Use exceptions for error handling, not return codes
  • Use type annotations consistently
  • Write comprehensive tests
  • Document all public APIs