Recording Experiment Configuration

Keep the choices that define a run in one explicit configuration record. Separate inputs and settings from computed results, pass configuration into the experiment, validate important constraints, and serialize simple values when the run must be recorded.

An experiment depends on choices made before it runs: a seed, number of trials, sample size, split fraction, input filename, or algorithm setting. If these values are scattered through a script, it becomes difficult to tell which choices produced a result.

An experiment configuration is an explicit record of the inputs and settings that define one run.

These names are clearer than unexplained literals inside a calculation, but a structured record keeps the related values together and makes them easier to pass, validate, display, and save.

Separate configuration from computed results

Configuration contains values known before the computation:

seed
trial count
sample size
split fraction
input data identifier
algorithm settings

Results contain values produced by the computation:

sampled values
measured duration
error
final score
output filename

For a coin simulation:

Keeping them separate answers two different questions:

  • Configuration: What did the program intend to run?
  • Measurements: What happened when it ran?

Do not place a result such as heads_count in the configuration merely because it is convenient to store everything in one object.

Define a configuration with a dataclass

A dataclass gives the record named fields and a visible type:

Create one configuration by naming every field:

Keyword arguments make the meaning of each value visible at the call site. The class annotation records the expected types for readers and static tools; as explained in the records chapter, type hints do not automatically validate values at runtime.

frozen=True prevents ordinary field reassignment:

config.seed = 22

This raises an error rather than silently changing the description of a run. It is most useful when configuration fields are simple immutable values. A frozen dataclass does not recursively freeze a mutable list or dictionary stored inside it.

Validate constraints close to the configuration

Type hints cannot express rules such as “trial count must be positive” or “probability must lie between zero and one.” A dataclass can check these rules in __post_init__:

Validation should reject an invalid run before it produces a misleading or partial result.

The closed interval for probability is deliberate:

0.0 <= heads_probability <= 1.0

Both 0.0 and 1.0 are valid probabilities, although they produce deterministic outcomes.

Pass configuration into the computation

A function that accepts one configuration makes its external settings visible:

The function reads all run settings from config. It does not depend on hidden module-level values and does not choose a new seed internally.

This pattern supports comparison:

The two configurations differ in one named field. A later comparison can state that trial count was the controlled change.

Derive values without hiding their source

Some operational values are calculated from configuration. A split boundary, for example, may depend on example count and training fraction:

The configuration should record the primary choices. The result record can also save an important derived value such as train_count so the exact boundary is visible.

Avoid storing two independent fields that should always agree:

Here, train_count conflicts with the other settings. Calculate it from the primary fields or validate the relationship explicitly.

Convert a dataclass to a saved record

dataclasses.asdict converts dataclass fields to an ordinary dictionary:

For simple Python strings, numbers, Boolean values, lists, and dictionaries, the resulting record can be written as JSON:

If configuration contains paths, enums, NumPy values, or custom objects, convert them to an intentional stable representation first. For example, store a path as a string and a NumPy scalar with .item().

Do not serialize fields merely because they exist. Exclude secrets, access tokens, and unnecessary personal information.

Run, display, and serialize one configuration

Edit one configuration value, then compare the printed configuration and measurements.

Run an experiment from validated configuration

The dataclass contains inputs known before the run. The function returns measurements, and the final record keeps the two groups separate.

Ready to run.

The JSON output is a representation of the run, not the configuration object used by the program. Keeping both concepts distinct prevents file-format details from controlling the internal design unnecessarily.

Use configuration to make changes explicit

When comparing two runs, begin with a baseline configuration:

Create a new configuration for the changed run:

Do not mutate the baseline and then describe it as though it were unchanged. A new immutable record preserves both sets of choices.

For a dataclass with many fields, dataclasses.replace can express one controlled change:

The result is a new configuration. The baseline remains unchanged.

Exercise: Identify a configuration field

Which value belongs in experiment configuration rather than measurements?

Choose the configuration value

Select one choice, then check.

HintUse the before-and-after distinction

Ask whether the value is known before the computation begins.

SolutionThe seed belongs to configuration

The seed defines the generator's starting state before the run. Observed count and elapsed time are measurements produced during the run.

Exercise: Interpret frozen configuration

What does @dataclass(frozen=True) provide for a configuration with integer and floating-point fields?

Choose the effect

Select one choice, then check.

HintDistinguish three responsibilities

Immutability, validation, and serialization are not the same feature.

SolutionFrozen prevents ordinary reassignment

The generated dataclass blocks statements such as config.seed = 22. Runtime constraints still need validation, and JSON output still needs conversion.

Exercise: Validate probability bounds

Which condition accepts every valid probability, including 0.0 and 1.0?

Choose the condition

Select one choice, then check.

HintInclude both endpoints

Use <= on both sides.

SolutionUse the closed interval

0.0 <= probability <= 1.0 accepts impossible events with probability zero, certain events with probability one, and every value between them.

Exercise: Avoid conflicting configuration fields

A configuration stores example_count=100, train_fraction=0.8, and train_count=70. What is the main problem?

Choose the problem

Select one choice, then check.

HintDerive the boundary

The stated rule gives int(100 * 0.8).

SolutionThe redundant fields disagree

The derived training count is 80, not 70. Store the primary choices and derive the count, or validate the relationship explicitly.

Exercise: Construct, validate, and record configuration

Complete the dataclass validation and create config so the program prints:

seed: 37
trial count: 12
record keys: ['heads_probability', 'seed', 'trial_count']

Build a serializable configuration

Ready to run.

HintComplete three separate steps

Check trial_count <= 0 and 0.0 <= heads_probability <= 1.0, construct CoinExperimentConfig(seed=37, trial_count=12, heads_probability=0.5), and call asdict(config).

SolutionValidate, construct, and convert

__post_init__ rejects an invalid count or probability before the experiment runs. The constructor gives every setting a named field, and asdict produces the three-field record.

Retain the configuration method

For each experiment:

identify choices known before the run
-> group them in a named configuration
-> validate constraints and relationships
-> pass configuration into the computation
-> derive dependent values explicitly
-> save configuration beside measurements
-> create a new record for each controlled change

Configuration makes an experiment's intended inputs visible. Saved results then show what those inputs produced.