BayBE (Bayesian Back End) is a Python library for Bayesian optimization and experimental design. It targets Python 3.10+.
| Role | Library |
|---|---|
| Data modeling | attrs (@define) |
| Serialization | cattrs + custom SerialMixin |
| Bayesian optimization | botorch, gpytorch |
| Tensor backend | torch |
| Numerics | numpy, pandas, scipy, scikit-learn |
| Testing | pytest, hypothesis |
| Linting/formatting | ruff, mypy, pyrefly, pydoclint, pyupgrade |
baybe/— Main source package with many subpackages containing structures and objects such as parameters, constraints, targets, etc.tests/— Test suite.conftest.pyhas the central fixture system.docs/— Sphinx documentation (Furo theme, MyST-Parser).docs/concepts/has concept-oriented pages;docs/components/has component reference pages.examples/— Runnable example scripts (jupytext percent format).benchmarks/— Benchmarking framework..github/workflows/— CI/CD (lint, test, docs, release, benchmark).
More specific conventions for subdirectories:
docs/AGENTS.md— Documentation conventions (Sphinx, MyST, user guide)examples/AGENTS.md— Example conventions (jupytext format, SMOKE_TEST)tests/AGENTS.md— Testing conventions (pytest, fixtures, parametrize, hypothesis)
- Campaign is the single stateful class: All other domain objects are frozen or
stateless after construction. Do not add mutable state outside
Campaign - Comp-rep vs. exp-rep boundary: The comp-rep transition happens in the surrogate
layer, not the recommender.
compis reserved for this — never reuse it for "component" or "comparison" - Composition over monolith: Use builder pattern (e.g., acquisition functions) and
factory classmethods (
from_product,from_dataframe,from_parameter) instead of if-else chains - Explicit over implicit: No silent errors — raise immediately. Validate eagerly at construction time. Side effects after validation only. Never fall back to degraded or "best effort" mode — if a dependency is missing, data is malformed, or a precondition is not met, abort. Fail before expensive downstream computation (surrogate fitting, acquisition optimization) when prerequisites are not satisfied
- Separation of concerns: Target transformation ("what/how to transform") is separate from objective ("how to combine targets"). Surrogate owns data scaling/transformation; recommender owns recommendation logic.
- Design for generalizability. Keep fields private when API may change
- Keep classes focused — attributes relevant to the general case only
- Prefer BayBE types over raw external types in interfaces
- Keep raw data types consistent; defer conversions to point of use
All domain classes use attrs @define. No dataclasses, no Pydantic.
- Immutable value objects (parameters, kernels, priors, transformations, objectives,
targets):
@define(frozen=True, slots=False). - Mutable stateful objects (campaign, surrogates, recommenders):
@define. slots=Falserequired withfrozen=Truewhencached_propertyis needed. Seeattrsissue #164- Also use
slots=Falsewhen monkeypatching is needed (e.g.,register_hooks)
Protocol(@runtime_checkable): External/duck-typed interfaces. Always__slots__ = ().- ABC: Shared behavior. Inherits from
ABCandSerialMixin - Concrete classes: Inherit from ABC.
- Use
field()withvalidator=,converter=,default=,factory=,alias=. - Private fields:
_prefix, typicallyinit=False. - Store each piece of information once — no data duplication.
- Use
attrs.evolve()for modified copies of frozen objects. - Use
on_setattrhooks for cache invalidation on mutable objects. ClassVar[bool]for capability flags (supports_transfer_learning, etc.).- Order class content like this: 1) Attributes, 2) validators and post_init, 3) properties, 4) methods. Within each group use alphabetical order.
String literals immediately below field declarations, blank lines between attributes.
name: str = field()
"""The name of the object."""
values: tuple[float, ...] = field(factory=tuple)
"""The possible values."""Every module using @define must end with:
gc.collect() with comment "Collect leftover original slotted classes..."
Name descriptively: from_product, from_dataframe, from_parameter, from_config,
from_json, from_dict, from_preset.
Custom @classproperty from baybe.utils.basic for class-level computed properties.
| Element | Convention | Examples |
|---|---|---|
| Variables/functions | snake_case |
batch_size, add_measurements |
| Classes | PascalCase |
Campaign, BotorchRecommender |
| Constants | SCREAMING_SNAKE_CASE |
_RECOMMENDED, _TYPE_FIELD |
| Private members | _ prefix |
_cached_recommendation, _fit() |
| Booleans | is_/has_/supports_ |
is_numerical, supports_transfer_learning |
| Counts | n_ prefix |
n_batches_done, n_grid_points |
| Protocols | Protocol suffix |
RecommenderProtocol |
| Default/validator methods | _default_<attr> / _validate_<attr> |
| Term | Reserved Meaning |
|---|---|
task |
Transfer learning context only |
comp / comp_rep |
Computational representation |
exp_rep |
Experimental representation |
configuration |
Assignment of values to ALL parameters (not individual values) |
- Full words in public API:
searchspace,objective,measurements. - Short forms in local contexts:
df,p(parameter),t(target),rec. - Established:
comp_rep,exp_rep,acqf.
- Use
self.__class__.__name__not hardcoded class names in errors/repr. - No private field names (
_attr) in user-facing messages — usefields(type(self)).attr.alias. - Method names start with verbs. Comments capitalize first word.
- Always capitalize words that correspond to names of inventors, e.g.
Bayesian,BooleanorGaussian - Use
make_prefix for functions/methods that construct and return a new object. Useget_prefix for functions/methods that retrieve and return an already existing object. Never usebuild_as a substitute for either.
- Full coverage: All signatures including returns. Every field annotated.
- Uses
from __future__ import annotationswhere needed (PEP 563). - Modern syntax:
X | Y(notUnion),X | None(notOptional). TypeAliasfor complex types,ClassVar[T]for class metadata.TypeVarwith_cosuffix for covariant.Literal[...]for constrained strings.NoReturnfor deprecation properties that always raise.- Always use
@typing_extensions.overrideon overridden methods (enforced by mypyexplicit-override). - Use positional-only (
/) and keyword-only (*) markers where appropriate. - Heavy/optional imports for type annotations go in
if TYPE_CHECKING:blocks. # type: ignoremust include specific error code and explaining comment.- Use
==for equality,isfor identity/sentinels. Prefertupleoverlistfor immutable returns. Prefer generic types (e.g.,CollectionoverSequencewhen onlyinis needed).
- Order (enforced by ruff isort):
__future__| stdlib | third-party | local.import Xbeforefrom X import ...within each group. - Lazy imports:
torch,botorch,gpytorch,scipy,sklearnmust be lazy-loaded inside function bodies. CI tests asserttorchis not loaded after importing BayBE objects. Non-negotiable. TYPE_CHECKINGguard: Imports only for annotations go insideif TYPE_CHECKING:.- Aliasing: BoTorch types use
Boprefix (e.g.,BoAcquisitionFunction). - Remove unused imports immediately. Module-level imports preferred except for optional dependency gating.
- Module: One-line description.
- Class: Describe purpose. Attribute docs go below each field, not in class docstring.
- Method/function:
Args:,Returns:,Raises:sections. - Types from annotations only (not in docstrings). All raised exceptions documented.
- Imperative mood for summary line (D401). D105/D107 exempted.
- Even private properties and
cached_propertyneed docstrings. - Overridden methods inherit docstrings via
@override. - Sphinx roles for cross-refs:
:func:,:class:,:meth:. Double backticks for literals. - Attrs validators get
# noqa: DOC101, DOC103(pydoclint confused by(self, attribute, value)signature):@some_field.validator def _validate_some_field(self, attribute, value): # noqa: DOC101, DOC103 """Validate some_field.""" if value < 0: raise ValueError("some_field must be non-negative.")
# TODO:— planned improvements.# IMPROVE:— enhancement ideas (project-specific).# FIXME[tag]:— known problems.# NOTE:— important context.##### Section #####separators in rare occasions where justified.# >>>>>>>>>> Deprecation/# <<<<<<<<<< Deprecationblock delimiters.- Do not use other comment styles such as
-----.
- Custom errors and warnings can be found in
baybe/exceptions.py. - f-strings with self-documenting expressions:
f"{batch_size=}". - Always chain exceptions:
raise ... from ex. - No partial results: Either the full operation succeeds or it raises. Invalid inputs must not produce partial or silently degraded outputs.
- Warnings originate at source level. Use specific warning classes, not bare
Warning.
- Built on
cattrswith central globalConverterinbaybe/serialization/core.py. SerialMixinprovidesto_dict()/from_dict(),to_json()/from_json().- Polymorphic deserialization via
"type"field on abstract bases. - DataFrames serialized via pickle + base64.
Campaignincludes"version"field."constructor"key enables alternative classmethod deserialization.- Register custom cattrs hooks at module level after class definition.
- Always copy caller-provided dicts before
pop()/mutation.
Three tiers:
- Soft:
warnings.warn(..., DeprecationWarning)— message says what to use instead and when removal happens. - Hard:
DeprecationError— fully removed features that must interrupt execution. - Serialization-level: Custom cattrs hooks redirect legacy class names.
- Deprecated class names become wrapper functions that warn and delegate.
- Mark blocks with
# >>>>>>>>>> Deprecation/# <<<<<<<<<< Deprecation. - Old env var names auto-translated with warning at settings init.
- Private cross-module imports acceptable temporarily during deprecation; clean up when expired.
- Test in
tests/test_deprecations.py.
- Inline validators:
field(validator=(instance_of(str), min_len(1))),in_(),deep_iterable(), customfinite_float,gt(). - Method validators:
@_field.validatorwith# noqa: DOC101, DOC103for validators needingselfaccess. - Cross-field:
__attrs_post_init__when validation involves multiple fields. - Converters:
field(converter=to_searchspace)for automatic type coercion. - Reusable validators in
baybe/utils/validation.py:finite_float,non_nan_float,non_inf_float,validate_not_nan,validate_target_input,validate_parameter_input,validate_object_names. - Cache invalidation:
on_setattrhooks on mutable fields.
- Detection (
baybe/_optional/info.py):importlib.util.find_spec()sets Boolean flags (CHEM_INSTALLED,ONNX_INSTALLED, etc.) without importing. - Guarded imports (
baybe/_optional/<dep>.py): Import or raiseOptionalImportErrorwith pip install instructions. - In tests:
@pytest.mark.skipif(not CHEM_INSTALLED, ...). Usepytestmarkfor module-level skips.
- Every package defines
__all__(always alist). Only concrete user-facing classes exported. - Top-level exports:
Campaign,Settings,active_settings,__version__. - Subpackage
__init__.pyre-exports for convenient imports. - Aliases allowed (e.g.,
EI = ExpectedImprovement).
| Tool | Config | Purpose |
|---|---|---|
flake8+pydoclint |
pydoclint.toml |
Docstring consistency (DOC rules only) |
mypy |
mypy.ini |
Type checking. explicit-override, unused-ignore enabled. |
pyrefly |
pyproject.toml [tool.pyrefly] |
Secondary type checker. Run via pyrefly check. |
pyupgrade |
.pre-commit-config.yaml |
Python 3.10+ syntax |
ruff |
ruff.toml |
Lint (D, E4, E7, E9, E501, F, I, W, PL) + format. Line length 88. |
zizmor |
.pre-commit-config.yaml |
GitHub Actions security audit |
Pre-commit hooks: pydoclint, ruff (lint+format), uv-lock, pyupgrade, zizmor.
Tox environments: fulltest, coretest, lint, typecheck, audit, docs.
Coverage: 70% overall, 45% per-file minimum.
CI runs on push/PR to main/dev/**: changelog check, lint, typecheck, audit,
coretest, fulltest. Docs and benchmarks on releases.
For each development, ensure tox -e typecheck-py310 runs without problems.
The entire test suite is too expensive to run all the time, but you should select
relevant tests from tests/ or newly created tests to validate your developments.
For a full list of available tox environments and developer commands, see
CONTRIBUTING.md.
- Feature branches from
main.dev/**for development. Naming:bug/,refactor/,feature/,benchmarks/,docs/. - Rebase preferred for linear history.
- CHANGELOG.md updated in every PR (CI enforced). Specific entries, complete sentences. Commit named "Update CHANGELOG" as last commit.
- Use imperative in commit header, e.g. "Add", "Fix", "Rework", "Handle", "Adjust", etc.
- Keep commit body short and informative. Do not add commit body if it has no additional info compared to the header.
- Pre-commit must pass. Clean history: squash add/revert pairs, no debug prints.
- Create a separate commit for logical steps, neither per-file nor one giant commit.
- When implementing a fix for some pre-existing issue, make a separate commit with explanation.
- No monolithic if-else chains — use builder/strategy patterns.
- No position-based argument matching — use name-based.
- No dead/unreachable code.
- No data duplication.
- No eager imports of heavy deps (torch, scipy, sklearn, botorch) — lazy-load.
- Do not mix reserved terminology (
task= TL only,comp= comp-rep only). - No hardcoded enum values in comments — link the enum.
- No private field names in user-facing messages — use public alias.
- No hardcoded class names in repr/errors — use
self.__class__.__name__. - No silent errors. No mutation of caller-provided dicts.
- No silent defaults or "best effort" fallbacks — if input is invalid, raise.
- No proceeding past failed preconditions into expensive computation.
- No stale cross-references. No inconsistent terminology in docstrings.
- No conftest pollution — prefer local fixtures.
- Tests must test what they claim. No duplicated test logic — parametrize.