Skip to content

← Modelling Data with Dataclasses step 4 of 28

Medium Primitives

Hide Secret Fields When You Print a Record

Your service logs its settings when it starts, and the log line shows the database password:

print(SAMPLES["endpoint"])
# Endpoint(host='db.internal', port=5432, password='hunter2')

Write redacted_repr, which returns the same text with '***' in place of each field marked as secret.

solve("endpoint")
# "Endpoint(host='db.internal', port=5432, password='***')"

Three field options

The starter's classes use three options of dataclasses.field():

  • repr=False leaves the field out of the generated __repr__.
  • compare=False leaves the field out of the generated ==. The field still appears in the repr.
  • metadata is a read-only mapping that you attach to a field for your own code to read. The dataclass ignores it. Here, metadata={"sensitive": True} marks a secret.

Metadata hides nothing by itself. print() on an Endpoint still shows its password. Only code that calls redacted_repr hides it.

What to write

Fill in redacted_repr(obj). Go through dataclasses.fields(obj) in declaration order, and apply these rules to each field, in this order:

  1. If f.repr is False, leave the field out.
  2. If bool(f.metadata.get("sensitive", False)) is True, write the value as '***'. Include the quotes, so the text is the same as repr("***").
  3. If the value is a dataclass instance, call redacted_repr on it. Test for one with is_dataclass(value) and not isinstance(value, type), because is_dataclass is also true for a dataclass class.
  4. Otherwise, write repr(value).

Join the name=value parts with a comma and a space, and wrap them as ClassName(...). Use type(obj).__name__, not __qualname__. A record with no fields to show becomes ClassName().

Keep the classes and solve as they are. solve takes a name from SAMPLES and calls your function.

Edge cases

  • A secret field shows '***' even when its value is empty.
  • A field that is both secret and repr=False does not appear, because rule 1 comes first.
  • A secret field that holds a dataclass shows '***'. Do not look inside it.
  • Recurse only into a field whose value is a dataclass. Lists, tuples and dicts keep their normal repr, even when they hold a secret.

For Credentials, user appears as normal, token becomes '***', and api_key does not appear. endpoint recurses, so its password becomes '***'. headers keeps its dict repr, and attempts appears even though it has compare=False.

Your code must pass mypy --strict. DataclassInstance is a type that exists only for the type checker and means "any dataclass instance". That is why the starter imports it under if TYPE_CHECKING: and writes it in quotes.