All.coding style guidelines.reference
General#
Spelling#
Linter#
- Spell commands in lower case and programs with the first letter in upper case
- E.g.,
gitas a command,Gitas a program - E.g., capitalize the first letter of
Python
- E.g.,
- Spell
Linterwith the first letter in upper case and do not use an article- E.g.,
Linterinstead ofthe Linter
- E.g.,
- Capitalize
JSON,CSV,DBand other abbreviations - Represent intervals with
[a, b), (a, b], (a, b), [a, b], not[a, b[ - Write
hyperparameterwithout a hyphen - Use
Pythonfor scripting and automation tasks
Python#
Naming#
LLM#
- Name functions using verbs and verbs/actions
- Good:
download_data(),process_input(),calculate_sum() - Good: Python internal functions as
__repr__,__init__are valid - Good: Functions names like
to_dict(),_parse(),_main()are valid
- Good:
- Name classes using nouns
- Good:
Downloader(),DataProcessor(),User() - Bad:
DownloadStuff(),ProcessData(),UserActions()
- Good:
- Name decorators with an adjective or a past tense verb
- Good:
timed,cached,logged - Bad:
time,cache,log
- Good:
- Variable and function names should not reference implementation details, and
things that can change or details that are not important
- E.g., the name of a variable should not include its type
- Good:
embeddings - Bad:
embeddings_list - Good:
data - Bad:
data_dict
- Good:
- E.g., the name of a variable should not include its type
- Abbreviations in the names of variables and functions should be avoided
- Exceptions are the following
dffor dataframesrsfor seriesidxfor indexidfor identifiervalfor valuevarfor variableargsfor arguments andkwargsfor keyword argumentscolfor columns androwfor rows
- Exceptions are the following
- Do not repeat in a function name what is already included in the library name
avoiding "code stutter"
- E.g., if using a library named
math, avoid naming a functionmath_calculate()- Good:
calculate() - Bad:
math_calculate()
- Good:
- E.g., if using a library named
Linter#
- Name executable Python scripts using verbs and actions
- E.g.,
download.pyand notdownloader.py
- E.g.,
- Name non-executable files using nouns
- E.g.,
downloader.py
- E.g.,
- Use
dirand notdirectoryorfolder- E.g.,
dir_path
- E.g.,
- Use
file_nameand notfilename- E.g.,
file_namefor storing the name of a file
- E.g.,
- Use
dir_nameand notdirname- E.g.,
dir_namefor storing the name of a directory
- E.g.,
- Use
timestampand nottsordatetime- E.g.,
event_timestamp
- E.g.,
- To refer to the name of a column, use
..._coland not..._col_nameor..._column- E.g.,
age_colfor a column storing age values
- E.g.,
Docstrings#
LLM#
- All functions and methods must have a docstring
- Good:
def add(a, b): """ Add two numbers and return the result """ return a + b - Bad:
def add(a, b): return a + b
- Good:
-
The docstring must describe the goal of the function, the interface, and what the user needs to know to use the function
- Good: "Calculate the sum of two numbers and return the result."
- Good
def get_repository_settings( repo: github.Repository.Repository, ) -> Dict[str, Any]: """ Get the current settings of the repository. :param repo: GitHub repository object :return: dictionary containing repository settings """ - When the output is a tuple indent like:
:param mode: format of the output: - `list`: indents headers to create a nested list - `headers`: uses Markdown header syntax (e.g., '#', '##', '###'`)
-
The docstring must use imperative form, whenever possible
- Good: "Calculate the sum of two numbers and return the result."
- Bad: "Calculates the sum of two numbers and returns the result."
- The docstring should not describe implementation details that can be changed
- Good: "Sort the list of integers in ascending order."
- Bad: "Use the quicksort algorithm to sort the list of integers in ascending order."
- Follow this example for indentation of parameter descriptions:
- Good
```python
:param param1: a very very long param description that
continues into a second line
:param param2: a param with two possible values
- first value description
- second value description that is very long and continues into a second line ```
- Good
```python
:param param1: a very very long param description that
continues into a second line
:param param2: a param with two possible values
- Adding examples (e.g., of input and output) to the docstring is encouraged
- Good
# Example usage: result = add_numbers(3, 5) # The result is 8.
- Good
- References to variables, file paths, functions, classes, etc. should be
wrapped in backticks
- Good: "The
add_numbers()function takes two argumentsaandb." - Bad: "The add_numbers() function takes two arguments a and b."
- Good: "The
- References to values should be wrapped in single ticks
- Good '//' with '# '
- Multi-line representations of data structures (e.g., an output example) should
be wrapped in triple backticks
- Good
{ "name": "John", "age": 30, "city": "New York" }
- Good
Linter#
- The first docstring line is followed by a blank line and then, optionally, by a longer description (possibly on multiple lines) with a more detailed explanation of what the function does
- The more detailed description is followed by a blank line and then the param
and return description section in reST style
- Use lowercase after
:param XYZ: .../:return:unless the description starts with a proper noun
- Use lowercase after
- Do not mention default values of parameters in parameter descriptions
- Docstrings should be wrapped in triple quotation marks (
""")- The opening and closing triple quotation marks should be located on their own separate lines
Comments#
LLM#
- Add a comment for every logically distinct chunk of code, spanning 4-5 lines
-
Use comments to separate chunks of code instead of blank lines
- Good:
function1() # Then do something else. function2() - Bad:
function1() function2()
- Good:
-
Do not use inline comments; every comment should be on its own separate line, before the line it refers to
- Good:
# Grant access to admin panel access_admin_panel(). if user.is_admin(): - Bad:
if user.is_admin(): # Check if the user is an admin access_admin_panel(). - In
if-elif-elsestatements, the comments are placed underneath each statement in order to explain the code that belongs to each statement in particular- Good:
if ...: # Do this else: # Do that
- Good:
- Good:
- Avoid referring to the type of a variable in the comments
- Keeps comments focused on functionality rather than implementation specifics
- Good: "Store the user's age for validation."
- Bad: "Store the user's age as an integer for validation."
- Do not include implementation details in comments
- Describe "what" and "why" the code does something and not "how" the code does it
- Ensures comments remain relevant even if the implementation changes
- If some code is commented out in a PR, a comment should be added to explain
the reason why
- Provides context for future reference and helps other developers understand the decision
- E.g., "This section is commented out due to a known bug that needs fixing" or "Temporarily disabled for performance testing"
Code Implementation#
LLM#
-
Add type hints only to the function definitions, if they are missing.
- Good:
def process_data(data, threshold=0.5): results = [] for item in data: if item > threshold: results.append(item) return results - Bad:
def process_data(data: List[float], threshold: float = 0.5) -> List[float]: results: List[float] = [] for item in data: if item > threshold: results.append(item) return results
- Good:
-
Avoid complex assignments into if-then-else statements.
- Good:
capitalized_parts = [] for w in parts: if is_first_or_last or w.lower() not in small_words: w_out = w.capitalize() else: w_out = w.lower() capitalized_parts.append(w_out) -
Bad:
capitalized_parts = [ w.capitalize() if is_first_or_last or w.lower() not in small_words else w.lower() for w in parts ]to: - Good:
- Bad:if i == 0: is_first_or_last = True elif i == len(tokens) - 1: is_first_or_last = True elif i > 0 and not re.search(r'\w', tokens[i - 1]): is_first_or_last = True elif i < len(tokens) - 1 and not re.search(r'\w', tokens[i + 1]): is_first_or_last = True else: is_first_or_last = Falseis_first_or_last = (i == 0 or i == len(tokens) - 1 or (i > 0 and not re.search(r'\w', tokens[i - 1])) or (i < len(tokens) - 1 and not re.search(r'\w', tokens[i + 1])))
- Good:
-
Provide clear and informative error messages in exceptions using f-strings
- Good:
raise ValueError(f"Invalid server_name='{server_name}'") - Good:
raise TypeError(f"Expected type int, but got {type(var).__name__}")
- Good:
- Use complete
if-elif-elsestatements instead of a sequence ofifstatements- Ensure logical flow and clarity in conditional statements
- Good: ```python if condition1:
Execute block for condition1.#
... elif condition2:
Execute block for condition2.#
... else:
Execute block if none of the above conditions are met or raise an#
exception.#
``` - Compile a regex expression only if it's called more than once
- Optimize performance by compiling regex expressions that are reused
- E.g.,
import re pattern = re.compile(r'\d+') if pattern.match(string): # Do something.
- Use
if var is Noneto check ifvarisNoneinstead ofif not var- Good:
if my_variable is None: - Bad:
if not my_variable:
- Good:
- Use
isinstance()instead oftype()to check the type of an object- Good:
if isinstance(obj, str): - Bad:
if type(obj) == str:
- Good:
- Do not use
from ... import ..., unless it is thetypingpackage, e.g.,from typing import Iterable, List- Good:
from typing import Dict, Tuple - Bad:
from os import path
- Good:
- Always import with a full path from the root of the repo / submodule
- Good:
import myproject.module.submodule - Bad:
from submodule import my_function
- Good:
Code Design#
LLM#
- Follow DRY principle (Don't Repeat Yourself):
- Factor out common code in a separate function/method
- Do not copy-and-paste parameter descriptions; instead, write them in only
one function and put a reference to it in the other functions where the same
parameters are used
- E.g., "See
func_name()for the param description"
- E.g., "See
- Avoid redundancy in code logic and comments
- Keep public functions in an order representing the typical flow of use:
- Common functions, used by all other functions
- E.g., utility functions like
log_message(),validate_input()
- E.g., utility functions like
- Read data
- Good:
read_csv(),load_json()
- Good:
- Process data
- Good:
clean_data(),transform_data()
- Good:
- Save data
- Good:
write_csv(),export_json()
- Good:
- Common functions, used by all other functions
- Ensure that function names are descriptive and convey their purpose
- Use comments to explain complex logic or calculations
- Implement error handling to manage exceptions and edge cases
- Use inheritance or composition to reuse code in object-oriented programming
Linter#
- Order functions / classes in a topological order so that the ones at the top of the files are the "innermost" and the ones at the end of the files are the "outermost"
- Use banners to separate large sections of code, e.g.: ```python
#
Read data.#
#
```
- The text inside the banner should start with a capital letter and end with a
period
Imports#
Linter#
- All imports should be located at the top of the file
- Each module that can be imported should have a docstring at the very beginning
describing how it should be imported
- Linter adds it automatically
- No import cycles should be introduced by the changes in the PR
Type Annotations#
LLM#
- For type hints use use
List,Dict, andTupleto provide more explicit type information and help with static type checking- E.g.,
List[int]instead oflist - E.g.,
List[str]instead oflist - Use
Dictinstead ofdict- E.g.,
Dict[str, int]instead ofdict - E.g.,
Dict[int, List[str]]instead ofdict
- E.g.,
- Use
Tupleinstead oftuple- E.g.,
Tuple[int, str]instead oftuple - E.g.,
Tuple[str, List[int]]instead oftuple
- E.g.,
- E.g.,
Linter#
- All functions and methods, including constructors, must have type annotations
for all the parameters and returned structures
- Use
-> Noneif a function doesn't return anything - The only exception are invoke tasks, i.e. functions with the
@taskdecorator, they shouldn't have type annotations
- Use
- Type annotation
Anyshould be avoided, if possible
Functions#
LLM#
- Avoid pure functions without side effects, i.e., for the same input arguments, the returned value should not change (in contrast to functions that rely upon external state)
- Functions should not modify the function inputs
- E.g., if a function
f()accepts a dataframedfas its argument, thenf()will not modifydfbut make a copy and work on it - This ensures that the original data remains unchanged and can be reused
- E.g., if a function
- To maintain clarity and consistency in function definitions, use the following
order of parameters in a function declaration:
- Input parameters
- Output parameters
- In-out parameters
- Default parameters
- Default parameters should be used sparingly and only for parameters that 99% of the time are constant
- All the default parameters should be keyword-only
- They should be separated from the other parameters by
*- Good:
def example_function(param1: str, *, default_param1: int = 10) - Bad:
def example_function(param1: str, default_param1 : int =10)
- Good:
- This ensures that default parameters are always explicitly specified by name, improving readability
- They should be separated from the other parameters by
- Do not use mutable objects (such as lists, maps, objects) as default value for
functions; instead, pass
Noneand then initialize the default parameter inside the function- E.g., instead of using a list as a default parameter, use
Noneand initialize the list inside the function: - Good:
def add_item(item: str, *, items: Optional[List[str]]) -> List[str]: if items is None: items = [] items.append(item) return items
- E.g., instead of using a list as a default parameter, use
- Do not use a boolean parameter as a switch controlling some function behavior;
instead, use a string parameter
mode, which is allowed to take a small well-defined set of values- Good:
def process_data(mode: str = 'fast'):wheremodecan be'fast','slow', etc
- Good:
- For functions dealing with dataframes, avoid hard-wired column name
dependencies; instead, allow the caller to pass the column name to the
function as a parameter
- E.g.,
def calculate_average(df: pd.DataFrame, column_name: str):
- E.g.,
- Do not put computations of the output together in a
returnstatement, instead, compute the output first, assign it to a variable, and then return this variable- Good
result = compute_value() return result - Bad
return compute_value()
- Good
- A function should have a single exit point, i.e., one single line with
return- Good:
python def calculate_total(price, tax): total = price + (price * tax) return total - Bad:
python def calculate_total(price, tax): if price > 0: return price + (price * tax) else: return 0
- Good:
- A function should ideally return objects of only one type (or
None) - When calling a function, assign all the input parameter values to variables on
separate lines and then pass these variables to the function
- Good:
param1 = 10 param2 = 11 result = my_function(param1, param2) - Bad:
result = my_function(10, 11)
- Good:
- Explicitly bind default parameters, i.e., specify the parameter name when
calling a function, and do not bind non-default parameters
- Good:
func(10, 20, param3=30) - Bad:
func(10, 20, 30)
- Good:
Scripts#
Linter#
- Use Python and not bash for scripting
- All Python scripts that are meant to be executed directly should:
- Be marked as executable files with
> chmod +x foo_bar.py - Have the standard Unix shebang notation at the top:
#!/usr/bin/env python - Use the following idiom at the bottom:
python if __name__ == "__main__": ... - Use
argparsefor argument parsing
- Be marked as executable files with
Misc#
Linter#
- If a PR includes renaming a file, variable, parameter, function, class, etc., then all the instances and references to it throughout the codebase should be updated
Unit Tests#
Rules#
LLM#
- A test class should test only one function or class to help understanding test failures
- A test method should only test a single case to ensures clarity and precision
in testing
- E.g., "for these inputs the function responds with this output"
- A unit test should be independent of all the other unit tests
- Ensures that tests do not affect each other and can be run in isolation
- If there is a lot of common code across individual test methods, it should be
factored out in a helper method within the test class
- Reduces redundancy and improves maintainability of the test code
- E.g., a
setUpmethod to initialize common test data or configurations
- If some code needs to be repeated at the beginning / end of each test method,
it should be moved to
set_up_test()/tear_down_test()methods and the following idiom should be added to the test class:python @pytest.fixture(autouse=True) def setup_teardown_test(self): # Run before each test. self.set_up_test() yield # Run after each test. self.tear_down_test() - Each test method should have a docstring describing briefly what case is being
tested
- E.g., "Tests the addition of two positive integers."
- E.g., "Verifies that an exception is raised when dividing by zero."
- Test methods should have type hint annotations
- E.g.,
def test_addition(self) -> None:
- E.g.,
- Do not create temporary files for tests (e.g., with
tempfile) but usehunittest.TestCase.get_scratch_space()instead - If the input to the test is a large piece of code/text, it should be moved to
a separate file in the
inputdir corresponding to the test- E.g.,
outcomes/<TestClassName.test_method_name>/inputand read through the functionself.get_input_dir()ofTestCase - This approach allows for easy updates and modifications to test inputs without altering the test code itself
- E.g.,
- Do not use pickle files for test inputs
- Use JSON, YAML, CSV files for test inputs as they are more secure and human-readable
- In every test method separate logically distinct code chunks to prepare the
inputs, run the tests, and check the outputs using comments like below:
- E.g.,
# Prepare inputs. input_data = [1, 2, 3] # Run test. result = my_function(input_data) # Check outputs. self.assert_equal(result, expected_output)
- E.g.,
- Do not use
hdbg.dassertin testing but useself.assert*()methods - Prefer
self.assert_equal()instead ofself.assertEqual()- Always use actual and then expected value
- E.g.,
self.assert_equal(actual, expected)
- Use strings to compare actual and expected outputs instead of data structures
- E.g., use
self.assert_equal(str(actual_list), str(expected_list))
- E.g., use
- Use
self.check_string()to compare the actual output to a golden output in theoutcomesdir, when the output is large or needs to be modified easily- E.g.,
self.check_string(actual_output)
- E.g.,
- When testing for an assertion, check that you are getting the exact exception
that is expected
# Make sure function raises an error. with self.assertRaises(AssertionError) as cm: config_list.configs = configs act = str(cm.exception) self.check_string(act, fuzzy_match=True)
Linter#
- If a unit test is renamed or removed in a PR, the corresponding files in the
outcomesdir should also be renamed or removed
Notebooks#
General#
LLM#
- The code in the notebook should adhere to the same style and formatting guidelines as the code in libraries and scripts
- Common or general-purpose code should be factored out in functions and moved
from the notebook to a Python library, which would then be imported in the
notebook
- E.g., create a
utils.pyfile for helper functions
- E.g., create a
- Notebook cells should be idempotent, i.e., able to be executed multiple times
without changing their output value
- Avoid side effects such as modifying global variables or external states
- Ensure that cell execution order does not affect the results
- If the data is transformed, display a few lines to show the outcome
- E.g.,
df.head(3)to preview the first three rows of a DataFrame
- E.g.,
- If any data is discarded/filtered, display the percentage of the rows dropped
- E.g.,
print(f"Percentage of rows dropped: {dropped_rows / total_rows * 100:.2f}%") - Provides insight into data cleaning and filtering processes
- E.g.,
- Progress bars should be added where applicable
- Use libraries like
tqdmto show progress in loops or data processing tasks
- Use libraries like
Plotting#
LLM#
- Each plot should have a descriptive title to understand the context of the
plot at a glance
- E.g., "Monthly Sales Data for 2023" instead of just "Sales Data"
- Each plot should have axes labels
- E.g., label the x-axis as "Months" and the y-axis as "Revenue in USD"
- If there are several multiple data series on the same plot, it should have a legend
- In a plotting function,
plt.show()should not be added at the end- This allows for further customization or saving of the plot before displaying
- E.g., users might want to save the plot using
plt.savefig('plot.png')before showing it
- In a plotting function, the
axparameter should be exposed to allow users to customize the plot further- E.g., users can modify the axes limits or add additional annotations
- If a function plots multiple plots, they should be generally plotted in a
single figure
- E.g., use
plt.subplots()to create a grid of plots within a single figure
- E.g., use
Linter#
- The name of a notebook should generally be the same as the branch name, unless it's a Master notebook
- All notebooks should have a table of contents
- Linter automatically adds and updates the table of contents
- At the top of the notebook there should be a Markdown cell
# Description, followed by a Markdown cell with an explanation of the notebook's goal, what it does, etc. - Immediately below the description, there should be a Markdown cell
# Imports, followed by a code cell importing all the needed libraries- It should include autoreload modules to keep the local code updated in real
time:
python %load_ext autoreload %autoreload 2 - All the imports should be located in a single cell
- It should include autoreload modules to keep the local code updated in real
time:
- Below the cell with the imports, there should be a code cell that configures the logging and notebook style, and reports execution info: ```python
Configure logger.#
hdbg.init_logger(verbosity=logging.INFO) _LOG = logging.getLogger(name)
Print system signature.#
_LOG.info("%s", henv.get_system_signature()[0])
Configure the notebook style.#
hnotebook.config_notebook() ``` - The rest of the notebook should be clearly organized using Markdown cells with headings of different levels - There should be no errors in the executed notebook - Ideally, there should be no warnings in the executed notebook
Jupytext#
Linter#
- Each notebook must have an accompanying Python file, linked via
jupytext, which contains a synchronized copy of the notebook's code - The notebook and its paired Python file should share the same name, differing only in their file extensions
- Ensure that the code in the notebook and its paired Python file remains synchronized at all times
- If you update or delete the notebook, you must also update or delete its paired Python file, and vice versa
Markdown#
General#
Linter#
-
Names of documentation files should follow the format
docs/{component}/{audience}.{topic}.{diataxis_tag}.mdto help in organizing and categorizing documentation files effectively where:- The
{component}part specifies the part of the project the documentation is related to - The
{audience}part indicates who the documentation is intended for - The
{topic}part describes the subject matter of the documentation - The
{diataxis_tag}part categorizes the documentation according to the Diátaxis framework (e.g., explanation, tutorial) - E.g.,
docs/documentation_meta/all.diataxis.explanation.md
- The
-
All Markdown files should have a table of contents
- The linter automatically adds and updates the table of contents
-
There should be one and only one level 1 heading (with one
#) in a Markdown- The level 1 heading:
- Should clearly convey the primary topic or purpose of the document
- Serves as the main title of the document
- Should be the first line and located above the table of contents
- The level 1 heading:
Headers#
LLM#
- Do not use bold or italics in headings
- Use headers so that it's easy to refer to something by link
- Do not make the chunk of text in a header too small since we don't want to
have too many headers
- E.g., there should be at least 5-10 lines in each header
Linter#
- Headings are capitalized as a title
- E.g.,
Data schemainstead ofData Schema - The linter automatically formats them
- E.g.,
Text#
LLM#
-
Use "you" and not "we" or "one"
- Let's just be direct: no need to be passive-aggressive
-
Text should be reflowed to the maximum of 80 columns per line
- The linter performs this operation automatically
-
When describing a tool the format should be the following:
- A description of what the tool does
- A list of examples of invocations of a tool with:
- A comment on the command line
- The command line
- Its output if possible
- A copy-paste version of the tool interface running
-h
Linter#
- Code blocks should always be accompanied by language markers
- E.g.,
bash,python,text,markdown
- E.g.,