Styleguide#
Your contribution should fulfill the criteria provided below.
Styleguide for the codebase#
Functions have no side effect. : If you modify a mutable argument, make a copy at the beginning of the function.
Use good names for functions and variables : “You should name a variable using the same care with which you name a first-born child.”, Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship.
A bit more concretely, this means:
The length of a variable name should be proportional to its scope. In a list comprehension or short loop, i might be an acceptable name for the running variable, but variables that are used at many different places should have descriptive names.
The name of variables should reflect the content or meaning of the variable and not only the type. Names like
dict_list
would not have been a good name for theconstraints
.Function names should contain a verb. Moreover, the length of a function name is typically inversely proportional to its scope. The public functions like
maximize
andminimize
can have very short names. At a lower level of abstraction you typically need more words to describe what a function does.User facing functions should be generous regarding their input type. Example: the
algorithm
argument can be a string,Algorithm
class orAlgorithm
instance. Thealgo_options
can be anAlgorithmOptions
object or a dictionary of keyword arguments.User facing functions should be strict about their output types. A strict output type does not just mean that the output type is known (and not a generous Union), but that it is a proper type that enables static analysis for available attributes. Example: whenever possible, public functions should not return dicts but proper result types (e.g.
OptimizeResult
,NumdiffResult
, …)Internal functions should be strict about input and output types; Typically, a public function will check all arguments, convert them to a proper type and then call an internal function. Example:
minimize
will convert any valid value foralgorithm
into anAlgorithm
instance and then call an internal function with that type.Fixed field types should only be used if all fields are known. An example where this is not the case are collections of benchmark problems, where the set of fields depends on the selected benchmark sets and other things. In such situations, dictionaries that map strings to BenchmarkProblem objects are a good idea.
Think about autocomplete! If want to accept a string as argument (e.g. an algorithm name) also accept input types that are more amenable to static analysis and offer better autocomplete.
Whenever possible, use immutable types. Whenever things need to be changeable, consider using an immutable type with copy constructors for modified instances. Example: instances of
Algorithm
are immutable but usingAlgorithm.with_option
users can create modified copies.The main entry point to optimagic are functions, objects are mostly used for configuration and return types. This takes the best of both worlds: we get the safety and static analysis that (in Python) can only be achieved using objects but the beginner friendliness and freedom provided by functions. Example: Having a
minimize
function, it is very easy to add the possibility of running minimizations with multiple algorithms in parallel and returning the best value. Having a.solve
method on an algorithm object would require a whole new interface for this.Deep modules. : This is a term coined by John Ousterhout. A deep module is a module that has just one public function. This function calls the private functions (i.e. functions that start with an underscore) defined further down in the module and reads almost like a table of contents to the whole module.
Never import a private function in another module : By following this strictly, you can be sure that you can rename or refactor private functions without looking at other modules. Of course it is also not a solution to copy paste the function! If you would like to import a function that starts with an underscore, rename it.
All functions have a Google style docstring : The docstring describes all arguments and outputs. For arrays, please document how many dimensions and what shape they have. Look around in the code to find examples if you are in doubt. Example:
def ordered_logit(formula, data): """Estimate an ordered probit model with maximum likelihood. Args: formula (str): A patsy formula. data (str): A pandas DataFrame. Returns: res: optimization result. """ pass
In particular each docstring should start with a one liner that describes very concisely what the function does. The one liner should be in imperative mode, i.e. not “This function does” …” , but “Do …” and end with a period.
Unit tests : If you write a small helper whose interface might change during refactoring, it is sufficient if the function that calls it is tested. But all functions that are exposed to the user must have unit tests.
Enable pre-commit hooks by executing
pre-commit install
in a terminal in the root of the optimagic repository. This makes sure that your formatting is consistent with what we expect.Use
pathlib
for all file paths operations. : You can find the pathlib documentation hereObject serialization. : Pickling and unpickling of DataFrames should be done with
pd.read_pickle
andpd.to_pickle
.Don’t use global variables unless absolutely necessary : Exceptions are global variables from a config file that replace magic numbers. Never use mutable global variables!