Skip to content

Coding Style Conventions

himkt edited this page Feb 1, 2023 · 44 revisions

General Rules

  • We follow the coding style defined by PEP 8.
    • As an exception, we extend the maximum line length to 99 characters.
  • We have some additional, project-specific conventions.
    • Please refer to the "Optuna Specific Conventions" section for the details.
  • We use type hints that are compatibility with Python 3.5:

Optuna Specific Conventions

Block and inline comments are supposed to be sentence(s).

# This is a good example.
# bad example
# Bad example

Don't use type hints in examples.

For simplicity.

Good Example

def objective(trial):
    ...

Bad example

def objective(trial: optuna.trial.Trial) -> float:
    ...

Add _ prefix to the names of private methods, functions, fields, and classes.

Example

class PublicClass:  # This class is exposed to external libraries.
    def __init__(self):
        self.public_field = 10            # This field is supposed to be accessed from other libraries.
        self._package_private_field = 20  # This field is supposed to be accessed only within the same library.
        self._private_field = 30          # This field is supposed to be accessed only within the same class.

    def public_method(self):
        pass

    def _package_private_method(self):
        pass

    def _private_method(self):
        pass


class _PackagePrivateClass:  # This class is supposed to be accessed only within the same library.
    def __init__(self):
        self.package_private_field = 10  # This field can be accessed from any place within the same library.
        self._private_field = 20         # This field is supposed to be accessed only within the same class.

    def package_private_method(self):
        pass

    def _private_method(self):
        pass


class _PrivateClass:  # This class is supposed to be accessed only within the same module.
    def __init__(self):
        self.package_private_field = 10  # This field can be accessed from any place within the same module.
        self._private_field = 20         # This field is supposed to be accessed only within the same class.

    def package_private_method(self):
        pass

    def _private_method(self):
        pass


def _package_private_function():  # This function is supposed to be accessed only within the same library.
    pass


def _private_function():  # This function is supposed to be accessed only within the same module.
    pass

Testing

Prefer pytest unittest with standard assertions over unittest tests.

Good Example

def test_foo():
    ...
    assert actual == expected

Bad example

def TestFoo(unittest.Testcase):
    def test_foo(self):
        ...
        self.assertEqual(actual, expected)

Similarly, prefer pytest.raises for testing expected errors.

Docstrings

We follow Example Google Style Python Docstrings with a couple of exceptions:

  • Add Example: sections.
  • Args and Attributes sections always start with a new line.
  • No inline docstrings.
  • The __init__ method must be documented in the class level docstring, not as a docstring on the __init__ method.
  • Use sphinx-style links to Python objects.

Example

def example_function(param1: int, param2: str) -> bool:
    """An example of function docstrings.
    
    Example:
        Using `testsetup` and `testcode`.

        .. testsetup::
            import numpy as np
                
        .. testcode::
            x = np.zeros(10)

    Args:
        param1:
            The first parameter.
        param2:
            The second parameter.

    Returns:
        The return value. :obj:`True` for success, :obj:`False` otherwise.

    """
    return True
class ExampleClass(object):
    """The summary line for a class docstring should fit on one line.

    If the class has public attributes, they may be documented here
    in an ``Attributes`` section and follow the same formatting as a
    function's ``Args`` section.

    Properties created with the ``@property`` decorator should be documented
    in the property's getter method.

    The `__init__` method must be documented in the class level docstring,
    not as a docstring on the `__init__` method.

    Args:
        param1:
            Description of `param1`.
        param2:
            Description of `param2`. Multiple
            lines are supported.

    Attributes:
        attr1:
            Description of `attr1`.
        attr2:
            Description of `attr2`.

    """

    def __init__(self, param1: str, param2: Optional[int] = 0):
        self.attr1 = param1
        self.attr2 = param2

    @property
    def readonly_property(self) -> str:
        """Properties should be documented in their getter method."""
        return "readonly_property"

    @property
    def readwrite_property(self) -> List[str]:
        """Properties with both a getter and setter
        should only be documented in their getter method.

        If the setter method contains notable behavior, it should be
        mentioned here.
        """
        return ["readwrite_property"]

    @readwrite_property.setter
    def readwrite_property(self, value: int) -> int:
        value

    def example_method(self, param1: str, param2: int) -> bool:
        """Class methods are similar to regular functions.

        Example:
            Using `testsetup` and `testcode`.

            .. testsetup::
                import numpy as np
                
            .. testcode::
                x = np.zeros(10)

        Note:
            Do not include the `self` parameter in the ``Args`` section.
            (Instead of ``Note:``, you can use ``.. note::``.)

        Args:
            param1:
                The first parameter.
            param2:
                The second parameter.

        Returns:
            :obj:`True` if successful, :obj:`False` otherwise.

        """
        return True

Some practices in Optuna are listed below.

Exceptions

This section is optional and should be used carefully. It should be documented if any of the followings:

  • An error is non-obvious. Documentation helps users to understand what happens.
  • An exception is expected to be caught in usercode. Documentation helps users to know what exception they should catch.
  • When we indicate specifications. We may document exceptions typically in base classes.

Good Example

def some_function(x: float):
   """...

   ...

   Args:
       x:
           Positive real number.

   """

Bad Example

def some_function(x: float):
   """...

   ...

   Args:
       x:
           Positive real number.

   Raises:
       :exc:`ValueError`:
           ``x`` is negative.

   """

Experimental Feature

If you implemented an experimental feature (class, method or function), please specify @experimental decorator to it.

Features that we consider as "experimental" include:

  • Features that can be implemented in various designs & We don't know which design is the best or better.
    For example, HyperbandPruner was first implemented in pr#809 but got improved with the changes in a handful of pull requests such as pr#1171 and pr#1188.
  • Algorithms that haven't been benchmarked extensively thus may or may not be stable for some objective functions, i.e., basically works well.

Features that we don't consider as "experimental" include:

  • Features whose interface could change in the future due to the change of their dependencies.
    More specifically, integration modules such as PyTorchLightningPruningCallback are not "experimental" in Optuna.

Example

from optuna._experimental import experimental

# Function: https://github.com/optuna/optuna/blob/248892e2/optuna/integration/lightgbm_tuner/__init__.py
@experimental("0.18.0")
def train(*args: Any, **kwargs: Any) -> Any:
    ...

# Class: https://github.com/optuna/optuna/blob/8d9576d7/optuna/pruners/hyperband.py
@experimental("1.1.0")
class HyperbandPruner(BasePruner):
    ...

# Method: https://github.com/optuna/optuna/blob/6e31e242/optuna/progress_bar.py
class _ProgressBar(object):
    ...

    @experimental("1.2.0", name="Progress bar")
    def _init_valid(self) -> None:
        ...

Logging HOWTO

Basically, we follow the Python official document. This section describes what needs special attention.

When we issue a warning regarding a particular runtime event, we use the following rule.

  • If the issue is avoidable and the client application should be modified to eliminate the warning, please use warnings.warn().
  • If there is nothing the client application can do about the situation, but the event should still be noted, please use optuna.logging.Logger.warnings()

Example

import warnings

from optuna import logging

# Deprecate a feature which we cannot use the deprecation decorator, then use `warnings.warn()`
# https://github.com/optuna/optuna/blob/4d6e1c7e5f163744136dd1b67593d03cb977bc0f/optuna/cli.py
class _StudyOptimize(_BaseCommand):
    ...
    def take_action(self, parsed_args):
        # type: (Namespace) -> int

        message = (
            "The use of the `study optimize` command is deprecated. Please execute your Python "
            "script directly instead."
        )
        warnings.warn(message, DeprecationWarning)
        ...

# Warn the exceptional sampler is used for the `CmaEsSampler` due to out of bounds, then use `optuna.logging.Logger.warnings()`.
# https://github.com/optuna/optuna/blob/4d6e1c7e5f163744136dd1b67593d03cb977bc0f/optuna/samplers/_cmaes.py
_logger = logging.get_logger(__name__)


class CmaEsSampler(BaseSampler):
    ...
     def _log_independent_sampling(self, trial: FrozenTrial, param_name: str) -> None:
        self._logger.warning(
            "The parameter '{}' in trial#{} is sampled independently "
            "by using `{}` instead of `CmaEsSampler` "
            "(optimization performance may be degraded). "
            "You can suppress this warning by setting `warn_independent_sampling` "
            "to `False` in the constructor of `CmaEsSampler`, "
            "if this independent sampling is intended behavior.".format(
                param_name, trial.number, self._independent_sampler.__class__.__name__
            )
        )
    ...

Consistently use suggest_float

In docstrings and test cases, we prefer suggest_float to suggest_uniform, suggest_loguniform and suggest_discrete_uniform. See pr#2344.

Good example

def objective(trial: Trial) -> float:
    x = trial.suggest_float("x", 0, 1)
    y = trial.suggest_float("y", 1e-7, 1e-2, log=True)
    z = trial.suggest_float("z", 0, 10, step=0.5)
    ...

Bad example

def objective(trial: Trial) -> float:
    x = trial.suggest_uniform("x", 0, 1)
    y = trial.suggest_loguniform("y", 1e-7, 1e-2)
    z = trial.suggest_discrete_uniform("z", 0, 10, q=0.5)
    ...

Clone this wiki locally