faker-synthetic-data
Substitutes realistic replacement values for PII that a masking or de-identification step removed, nulled, or redacted, so a non-production dataset stays usable. Covers building an injective substitution map that keeps a shared identifier consistent everywhere it appears so joins survive; choosing deterministic (seeded) over random substitution, and the re-identification risk a shared or committed seed reintroduces, since a generator seed is a reproducibility control and not a cryptographic key; preserving field shape where a downstream system validates it, including check-digit values such as payment-card and national-ID numbers plus phone and postal formats; and why a value that merely looks realistic is not yet safe, leaving a residual re-identification measurement over the remaining quasi-identifiers. Use when a masking pipeline has nulled or dropped PII columns and the dataset now needs replacement values that keep cross-table joins intact.
Install with skills.sh (any agent)
npx skills add testland/qa --skill faker-synthetic-datafaker-synthetic-data
The axis this skill owns
A de-identification step has already run: direct identifiers are nulled, redacted, or replaced with a placeholder. The dataset is private but broken, because joins fail and validators reject empty fields. This skill covers putting usable values back and the privacy decisions that forces: keeping a shared identifier consistent so joins survive, deterministic versus random substitution, preserving a validated field shape, and proving the result is safe rather than merely realistic.
It deliberately does not cover the generic mechanics of a fake-data library: installation, the provider catalogue, per-locale tours, or factory patterns for building fixtures from nothing. Nor does it cover detecting PII or choosing between masking operators such as hashing, tokenisation, and nulling. Both happen before this step.
Minimum library surface
The worked example below uses only Faker.seed(n) (run reproducibility, a class method) and fake.unique.<method>() (injective values, raising UniquenessException once the value space is exhausted). The full mechanic table - instance vs class seeding, JS refDate, multiple-locale weighting, and Presidio's custom operator for driving substitution from an anonymiser - is in references/library-surface-and-seeding.md.
Step 1 - Build the substitution map before substituting anything
The masking literature requires masked values to stay consistent across databases that share a data element, and to be repeatable (the same input always yields the same output) yet not reversible to the original (en.wikipedia.org/wiki/Data_masking (opens in new window)).
So do not substitute row by row while streaming. Collect the distinct real values first, build one map, apply it everywhere. Two properties must hold:
The map is the sensitive artifact. Retaining it where the recipient can reach it makes the output pseudonymised, not anonymised: GDPR Article 4(5) treats data attributable to a subject only via separately-kept additional information as pseudonymised (gdpr-info.eu (opens in new window)), and Recital 26 counts data re-attributable through such additional information as personal data (gdpr-info.eu (opens in new window)). Keeping the map is a legitimate choice. Calling the result anonymous afterwards is not.
Step 2 - What a seed guarantees, and what it does not
Does. Reproduce the same generator sequence on a later run, for the same library version. Faker warns that "as we keep updating datasets, results are not guaranteed to be consistent across patch versions" (faker.readthedocs.io (opens in new window)); the JavaScript port warns "you may get different values for the same seed" after an upgrade (fakerjs.dev (opens in new window)).
Does not. Act as a key. Python Faker draws from a random.Random (faker.readthedocs.io (opens in new window)), and the standard library warns that the module's generators are not for security use and points to the secrets module for cryptographic uses (docs.python.org (opens in new window)). A seed offers no resistance to anyone holding it.
The four seeding strategies (per-value derived seed, global seed plus a stored map, global seed with the map discarded, unseeded random) are compared by re-identification exposure in references/library-surface-and-seeding.md. The per-value derived seed is the pattern to avoid. It needs no stored map, and it fails for the reason unsalted hashing of a low-entropy field fails: the input space is enumerable, so the mapping is rebuildable. If a stored map is unacceptable, the answer is a keyed transform, not a derived seed.
Step 3 - Preserve the shape the downstream system validates
Check-digit values. Payment card numbers, and national identifiers including Canadian social insurance numbers, Israeli and South African ID numbers, and Swedish personal identity numbers, carry a Luhn ("mod 10") check digit specified in ISO/IEC 7812-1 (en.wikipedia.org/wiki/Luhn_algorithm (opens in new window)). Faker exposes credit_card_number(card_type=...) accepting 'amex', 'visa', 'mastercard', 'discover', 'diners', 'jcb', and 'maestro' among others, but does not state whether the output carries a valid check digit (faker.readthedocs.io (opens in new window)), so assert it rather than assume it:
def luhn_ok(number: str) -> bool:
digits = [int(d) for d in number if d.isdigit()][::-1]
return sum(d if i % 2 == 0 else sum(divmod(d * 2, 10))
for i, d in enumerate(digits)) % 10 == 0
assert luhn_ok(fake.credit_card_number(card_type="visa"))Luhn "was designed to protect against accidental errors, not malicious attacks" (en.wikipedia.org/wiki/Luhn_algorithm (opens in new window)), so passing it proves the field parses, not that the value is safe.
Locale-bound formats. Phone numbers and postal codes are validated against the row's country, so pin one locale per row from that country column instead of relying on multiple-locale mode's weighted random draw.
Format plus reversibility. If the receiving system needs the original back later and the shape must survive, substitution is the wrong operator: NIST SP 800-38G, "Recommendation for Block Cipher Modes of Operation: Methods for Format-Preserving Encryption", specifies FF1 and FF3 for that case (csrc.nist.gov (opens in new window)).
Step 4 - Realistic is not safe, so measure the result
Residual quasi-identifiers. Substitution removes only the direct identifiers actually substituted. Birth date, postal code, sex, and admission date usually stay because they carry the analytical value, and in combination they still identify people. NIST SP 800-188, "De-Identifying Government Datasets: Techniques and Governance" (September 2023), treats transforming quasi-identifiers and running re-identification studies as part of de-identification, not optional extras (csrc.nist.gov (opens in new window)). The exit criterion is therefore a measurement over the remaining quasi-identifier columns (smallest equivalence-class size, count of unique rows), against Recital 26's test of whether means are "reasonably likely to be used", accounting for "the costs of and the amount of time required for identification" (gdpr-info.eu (opens in new window)).
Accidental collisions. Generators draw from fixed data, described in the JavaScript port's guide as "lists of names, words etc" (fakerjs.dev (opens in new window)), so a generated value can coincide with a real person's, including one in the source file. Diff substitutes against source values and fail on any intersection.
Worked example - two joined files
customers.csv holds email; orders.csv holds billing_email and joins on it. A masking step already dropped ssn and full_name.
import csv
from faker import Faker
fake = Faker("en_US")
Faker.seed(0) # run reproducibility only: not a secret, not derived from the data
def load(path):
with open(path, newline="") as f:
return list(csv.DictReader(f))
customers, orders = load("customers.csv"), load("orders.csv")
real = {c["email"] for c in customers} | {o["billing_email"] for o in orders}
email_map = {v: fake.unique.email() for v in sorted(real)}
assert len(set(email_map.values())) == len(email_map) # injective
assert not (set(email_map.values()) & real) # no collision with source
before = len({c["email"] for c in customers} & {o["billing_email"] for o in orders})
for c in customers:
c["email"] = email_map[c["email"]]
for o in orders:
o["billing_email"] = email_map[o["billing_email"]]
after = len({c["email"] for c in customers} & {o["billing_email"] for o in orders})
assert before == after, f"join key cardinality changed: {before} -> {after}"
print(f"substituted {len(email_map)} identifiers, join keys preserved: {after}")Discard email_map, or store it under separate access control. Never ship it beside the substituted files.
Expected output shape
| File | Column | Before | After |
|---|---|---|---|
| customers.csv | alice.tan@acme.example | xrogers@example.org | |
| orders.csv | billing_email | alice.tan@acme.example | xrogers@example.org |
| customers.csv | postcode | SW1A 1AA | SW1A 1AA (quasi-identifier, unchanged) |
substituted 12480 identifiers, join keys preserved: 9317
collision check vs source values: 0
residual quasi-identifiers not substituted: postcode, birth_date, sexThe third line is a handoff to a re-identification measurement: an open item, not a pass.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Seeding the generator from the real value before each call | The mapping is rebuildable by anyone with the code and a candidate value list | One global seed plus an access-controlled map, or a keyed transform |
| Substituting row by row while streaming, with no map | The same person gets a different substitute per occurrence and every join breaks | Collect distinct values, build the map once, then apply |
| Calling the extract anonymous while retaining the map | The map is the "additional information" of GDPR Article 4(5); the output is still personal data | Destroy the map, or label the output pseudonymised |
| Substituting a checksum-bearing field without asserting the check digit | Downstream validators reject rows, so the environment looks broken rather than masked | Assert Luhn or the field's own check rule before writing |
| Multiple-locale mode on a dataset with a country column | Locale is a weighted random draw per call, so a French row gets a Japanese postcode | Instantiate one locale per row from that row's country |
| Declaring done once the direct identifiers look fake | Birth date plus postcode plus sex still identify people | Measure equivalence-class sizes over the remaining quasi-identifiers before release |
| Never diffing substitutes against source values | Fixed name lists mean a substitute can be a real person in the same file | Fail the run on any intersection |
Limitations
faker-synthetic-data - library surface and seed-exposure detail
View source (opens in new window)faker-synthetic-data - library surface and seed-exposure detail
Deep reference for faker-synthetic-data (opens in new window). The runnable core - build the substitution map, assert injectivity, preserve check digits, measure residual risk - stays in SKILL.md. This file holds the mechanic tables it draws on.
Minimum library surface
| Mechanic | Behaviour | Source |
|---|---|---|
Faker.seed(n) | Class method; seeds the shared random.Random across all internal generators. Calling .seed() on an instance raises TypeError | faker.readthedocs.io (opens in new window) |
fake.seed_instance(n) | Creates and seeds a unique random.Random for one instance | faker.readthedocs.io (opens in new window) |
fake.unique.<method>() | Tracks values already returned; raises UniquenessException after repeated failures to find a new one | faker.readthedocs.io (opens in new window) |
| Multiple-locale mode | The proxy "randomly select[s] a generator using a distribution defined by the provided weights", so locale varies per call | faker.readthedocs.io (opens in new window) |
faker.seed(123) (JS) | Fixes the sequence; setting the seed again resets it. Date helpers also need refDate or faker.setDefaultRefDate() | fakerjs.dev (opens in new window) |
To drive substitution from an anonymiser, Microsoft Presidio exposes a custom operator taking a "lambda to execute on the PII data. The lambda return type must be a string", passed as OperatorConfig("replace", {"new_value": "BIP"})-style entries in the operators dict (presidio.dataprivacystack.org (opens in new window)).
Seed exposure matrix
The four seeding strategies referenced from Step 2 of the skill, ranked by the re-identification exposure each one leaves:
| Mechanism | Joins survive | Re-identification exposure |
|---|---|---|
| Seed derived per value, reseeding from the real value before each call | Yes | High. Anyone with the code, the library version, and a candidate list of real values re-runs the derivation and rebuilds the map. The seed reproduces the substitute; it does not conceal the input |
| One global seed plus a stored map | Yes | Equal to the exposure of the map. Control access to the map, not the seed |
| One global seed, map discarded after the run | Yes, within the run | Low from the seed alone: the substitute follows call order, not the input value, so the seed reconstructs nothing without the source data |
| Unseeded, random per occurrence | No | Low, but the dataset is unusable for anything relational |
Related skills
data-masking-techniques-reference
Pure-reference catalog of data-masking techniques and de-identification privacy models. Enumerates the seven canonical masking operators (substitution, shuffling, number/date variance, encryption, hashing, nulling, masking-out / character-scrambling) plus tokenisation, redaction, format-preserving encryption, and Microsoft Presidio's six built-in operators. Distinguishes reversible techniques (pseudonymisation candidates per GDPR Art. 4(5)) from irreversible techniques (anonymisation candidates), and maps them to NIST SP 800-188 privacy models - k-anonymity, l-diversity, t-closeness, differential privacy (deep model definitions in references/). Cites ISO/IEC 20889:2018 for the standard taxonomy. Use to pick the right masking operator per field type and risk level.
k-anonymity-verifier
Verifies that a masked dataset satisfies k-anonymity, l-diversity, and t-closeness by computing equivalence classes over chosen quasi-identifiers and reporting re-identification risk. Covers quasi-identifier selection heuristics, threshold guidance, pycanon API (k_anonymity / l_diversity / t_closeness / report), ARX Java API and GUI workflow, SmartNoise for differential-privacy comparison, and CI-gate integration. Distinct from data-masking-techniques-reference (which catalogs masking operators but defers k-anonymity measurement to dedicated tooling) and from presidio-pii-detection (which detects PII spans but offers no equivalence-class analysis). Use when you need to confirm whether a masked dataset meets a stated k, l, or t threshold before promoting it to a non-production environment.
pii-categories-reference
Pure-reference catalog of personally identifiable information (PII) categories across GDPR, CCPA/CPRA, NIST SP 800-122, and HIPAA. Defines what counts as personal data under each regime, enumerates the explicit identifiers each regulator lists (GDPR Art. 4(1) and Art. 9 special categories; CPRA sensitive personal information; NIST direct-identifier vs linkable distinction; HIPAA Safe Harbor 18 identifiers), and maps overlapping fields across jurisdictions so a masking pipeline knows which regulator's rules apply. Use as the authoritative source when authoring or reviewing masking rules, classifying a dataset's risk level, or scoping which fields a PII detector must catch.
pii-masking-pipeline-builder
Build-an-X workflow that produces a PII masking pipeline spec from a source-data inventory. Walks the author through (1) classifying each field against pii-categories-reference, (2) picking a masking operator from data-masking-techniques-reference, (3) deciding pseudonymisation (reversible, in GDPR scope) vs anonymisation (irreversible, out of scope), (4) ordering the pipeline (detect → operator → audit), and (5) emitting a deployable config for Presidio + Faker + Synthea wrappers. Output is a YAML pipeline spec plus a per-field rationale table. Use after classifying a dataset's PII risk; this is the workflow that translates classification into runnable masking config.
presidio-pii-detection
Author and run Microsoft Presidio PII detection - wraps presidio-analyzer (PII detector) + presidio-anonymizer (replace/redact/mask/hash/encrypt operators) for scanning datasets, log streams, and free-text fields. Covers AnalyzerEngine + AnonymizerEngine setup, built-in recognizers (PERSON, EMAIL_ADDRESS, CREDIT_CARD, US_SSN, IBAN_CODE, country-specific IDs across US/UK/Spain/Italy/Poland/Singapore/Australia/India and more), custom PatternRecognizer authoring, score thresholds, and CI gating. Use when scanning *existing* data for PII (vs synthesising fresh fixtures with synthetic-pii-generator).
synthea-healthcare-data
Author and run Synthea (MITRE's open-source synthetic patient population simulator) to produce HIPAA-safe synthetic medical records for testing health IT systems. Covers Gradle build, population-size and state-specific generation, FHIR R4 / STU3 / DSTU2 / C-CDA / CSV / CPCDS output formats, disease-module customisation, and the lifecycle-simulation approach (birth-through-death patient journeys with realistic demographics). Use when testing FHIR servers, EHR integrations, claims processing, or any health IT system that needs realistic patient records without HIPAA exposure (distinct from faker-synthetic-data which is generic; this is health-domain-specific).
test-data-governance-reference
Pure-reference catalog of test-data lifecycle governance: retention schedules for test datasets, cross-environment data-sharing agreements, deletion of test data containing real PII, refresh cadence, access controls, and the legal basis for each policy under GDPR Art. 5 storage limitation and NIST SP 800-122. Use when defining a data-steward role for test environments, authoring a retention policy for a test database, scoping a data-sharing agreement before promoting a dataset from production to staging, or determining the deletion timeline for any test fixture that contains live personal data.