CogniSkill · Python for IT Automation
Content review copy. This does not replace the site theme. For SC-300, this preview shows additions; the existing teaching is retained on import.
You have an asset register and a second inventory exported by a collector. They disagree. Before anyone updates ownership, retires a device or opens a service ticket, the team needs a dependable comparison: which identities exist in only one source, which shared identities have different fields, and whether the exports can support those conclusions at all.
This course builds that comparison. It is a practical continuation of Python Foundations: you should already understand variables, dictionaries, functions and basic file handling. You will design a data contract, reject ambiguous identities, compare normalized records, preserve source provenance, write a reproducible report and demonstrate that independent tests catch a real defect. The program does not contact devices or modify either inventory. No book purchase or cloud tenant is needed.
Use a local Python 3.10-or-later interpreter and a new practice directory. The project uses only the standard library. Save the supplied files there and record your actual interpreter version. A virtual environment is a useful habit for larger projects, but installing third-party libraries is unnecessary for this lab. These fixtures are synthetic and intentionally small enough to reconcile by hand.
Four chapters, with practical work
- Turn an automation request into a precise contract
- Parse, validate and index the evidence
- Build a complete, bounded reporting program
- Prove the rule and complete an independent project
Applied capability and learning route
Create a repeatable file-processing workflow with explicit idempotency and failure behavior.
Follow the lessons in order: establish the model, trace the supplied evidence, attempt the case, compare the worked solution, and complete the changed-condition project. The target is intermediate reasoning within this stated scope, not mastery of the entire profession or product. If a prerequisite is unfamiliar, pause and use the linked foundational teaching before attempting the project.
All teaching, explained answers and core synthetic practice are free. Some operational labs require separately available software, a licensed test tenant or an authorized lab; offline completion does not claim hands-on product proficiency.
How to complete this course
Study the worked examples, attempt the exercises before opening solutions, and submit your own project notes. Completion records are self-reported; practical competence requires the stated verification.
Turn an automation request into a precise contract
Define the operation before writing the loop
“Keep the inventories in sync” is not a specification. It could mean reporting differences, editing a directory, deleting unobserved devices or assigning owners automatically. Those operations have different evidence requirements and consequences. Our first automation does one narrowly stated job: compare two already exported snapshots and produce findings for human review.
Write the input, decision and output contract in your notes. Inputs are a registered inventory CSV, an observed inventory CSV and one collection manifest for each. The decision is a comparison by stable asset identity. Outputs are a findings CSV and a summary containing population counts and source hashes. Side effects are limited to a newly created output directory. Existing input files and existing output directories must remain unchanged.
A REGISTER_ONLY result means the asset identifier is present in the register and absent from the supplied observed export. It does not establish that the device was destroyed, disconnected or never existed. An OBSERVED_ONLY result means the reverse difference. It does not establish that the device is unauthorized. The names deliberately describe the evidence rather than asserting a lifecycle decision.
This distinction prevents an attractive automation demo from becoming an unsafe retirement rule. A collector could miss a whole network segment, a permission scope could hide devices, or an export could have stopped after its first page. A successful file read does not prove a complete population.
Give every record a stable identity
A display name such as Ana's laptop is useful for people but poor for reconciliation. Names change, and more than one device can have the same name. The fixture uses a source-defined asset identifier with the format A-001. In this fictional system the identifier is case-insensitive; leading/trailing whitespace is not meaningful. Therefore a-001 and A-001 represent the same identity after normalization.
That policy is specific to this system. Do not automatically uppercase real serial numbers, user names or arbitrary database keys without checking their contract. The owner field here is a stable identifier in the form u101 and is case-sensitive. It is not a display name or an email address. Site codes are the enumerated values HQ and BRANCH; letter case is normalized for those codes.
Normalization makes comparison consistent. Validation establishes whether a normalized value belongs to the accepted domain. Uppercasing unknown does not turn it into a valid asset identifier. Converting a missing owner into an empty string does not establish a valid owner. This program rejects either problem before issuing any inventory findings.
Prepare the four input files
Save the registered export as register.csv:
asset_id,owner_id,site
A-001,u101,HQ
A-002,u102,HQ
A-003,u103,BRANCH
A-004,u104,HQ
Save the observed export as observed.csv. Keep the spaces around its first asset identifier for the normalization exercise:
asset_id,owner_id,site
a-001 ,u101,hq
A-002,u999,BRANCH
A-003,u103,BRANCH
A-005,u105,HQ
Save the registered manifest as register-manifest.json:
{"complete":true,"row_count":4,"collected_at":"2026-10-01T09:00:00Z"}
Save the observed manifest as observed-manifest.json:
{"complete":true,"row_count":4,"collected_at":"2026-10-01T09:05:00Z"}
Duplicate manifest properties are rejected rather than allowing a later completion or count value to hide an earlier value. The manifests record a declared complete export, its expected number of parsed data records and its collection timestamp. true is a JSON Boolean; the word "true" in quotes is a string and does not satisfy this contract. row_count is an integer. A decimal, a negative number or a Boolean is not accepted as a row count. The program checks the exact type because Python's Boolean type has an integer relationship that can surprise a loose isinstance check.
Both timestamps contain a time-zone indication. An offset-free value cannot be reliably compared across collectors in different time zones. The code converts valid aware timestamps to UTC while preserving their distinct collection moments. It does not treat the report generation time as the time at which every asset was observed.
Predict the result manually
Make a five-row table of identities before running any code. A-001 and A-003 appear in both sources with matching fields after normalization. A-002 appears in both with two field differences: owner and site. A-004 appears only in the register. A-005 appears only in the observed export.
There should be four finding rows, but only three assets with findings. There are two field rows for A-002, one register-only row and one observed-only row. A dashboard that calls all four rows “affected devices” would overstate the device count. Decide the grain of each output measure before adding totals or percentages.
Check your population reconciliation
Registered assets: 4. Observed assets: 4. Shared assets: 3. Matching shared assets: 2. Mismatching shared assets: 1. Register-only assets: 1. Observed-only assets: 1. Field-mismatch rows: 2. Total finding rows: 4. Registered = shared + register-only; observed = shared + observed-only.
Checkpoint challenge
Change the observed first row to A-001,u101,HQ in a copy of the file. Predict which finding rows should change. None should change: the edit removes presentation differences that normalization already ignores. The raw file hash should change because the source bytes changed. Business result equality and byte-for-byte source equality are different claims.
Now change the first owner to U101. Predict whether the program should silently lowercase it. It should reject the row under the chosen case-sensitive owner contract. Changing the contract requires a documented decision and new tests, rather than a convenience fix inside a parser.
Reasoning model
An automation run should have a defined input set, transformation and commit point. Idempotency means repeating the same intended operation does not create additional unintended effects; it does not mean ignoring every duplicate-looking row. Choose a stable identity and document how changed content is handled. Stage output before replacing a destination, and never report success until the final write and validation complete.
Official reference for further study
Parse, validate and index the evidence
A CSV reader supplies fields, not trusted records
CSV parsing separates fields and handles quoted delimiters. It does not decide whether an asset identifier is valid, whether a key is unique or whether the collector returned every asset. Our contract requires the header asset_id,owner_id,site in that order. This deliberately strict first version makes schema drift explicit. Supporting reordered columns is a reasonable extension, but it needs its own uniqueness and missing-column checks.
Read the source bytes once and calculate the hash of those bytes. Decode them with the selected encoding and parse that snapshot. If you read once for parsing and read again for hashing, a source file could change between reads and the reported hash could describe different evidence. Reading a small fixture into memory makes the relationship clear; a large export would need an appropriate streaming or immutable-snapshot design.
DictReader maps fields to header names. Extra fields and missing fields can be represented in ways a careless loop ignores. Our row validator rejects both rather than accepting a damaged record. The parser's strict mode helps identify some CSV quoting errors, but it is not a universal assurance that any accepted file has the intended schema or business meaning.
Reject duplicate normalized identities
A common shortcut builds a dictionary with {row['asset_id']: row for row in rows}. If the same identity appears twice, the later row can overwrite the earlier one. The result looks unique precisely because the evidence of duplication was discarded.
This program checks for an existing key before inserting a row. A-001 and a-001 collide after normalization. Even identical duplicate rows are rejected because the one-record-per-asset contract has been violated. The program does not guess which row is current: neither source includes a per-asset modification time or an authoritative revision to justify that choice.
In some systems duplicate observations are legitimate—for example, one row per asset per timestamp. In that case the row grain is different, and an asset-only key is insufficient. Change the data model instead of pretending a multi-observation export is a unique current inventory.
Use sets for population questions and dictionaries for field questions
A set of registered identities and a set of observed identities let you ask three separate questions: intersection for shared identities, register-minus-observed for register-only identities, and observed-minus-register for observed-only identities. Set operations concern membership. Dictionaries let you retrieve the owner and site fields for a shared identity.
The example below is a small independent practice step. Predict its three outputs before executing it:
registered = {'A-001', 'A-002', 'A-003', 'A-004'}
observed = {'A-001', 'A-002', 'A-003', 'A-005'}
print(sorted(registered & observed))
print(sorted(registered - observed))
print(sorted(observed - registered))
Expected outputs are the shared list A-001, A-002, A-003; then A-004; then A-005. Sorting makes the display deterministic because membership sets are not a report ordering contract. Stable ordering helps a reviewer distinguish an actual changed finding from an arbitrary row reorder.
Swapping the operands changes the directional difference. Add a test that catches this mistake. A test containing identical source populations cannot distinguish register-only from observed-only logic, even if it exercises the comparison function.
Completeness is an evidence requirement
After parsing and rejecting duplicates, compare the number of accepted records with the manifest's expected count. If it differs, stop the report. If complete is false, stop the report even when the file is readable. Do not publish missing-device findings based on an export its own collector declares incomplete.
A matching count and a true completion flag are still declarations, not cryptographic proof of the real fleet. The upstream collector can be wrong. The program records provenance so an analyst can inspect the source and collection process. It does not fabricate certainty by labeling the summary “complete inventory truth.”
A declared complete zero-record inventory is allowed by this data contract. That can be legitimate for an empty practice source. In an operational system with thousands of expected assets, an unexpected empty export should trigger an additional population-change review. A machine cannot infer that expectation from an isolated file. Your final extension will design that review gate without making the first report destructive.
Practice: diagnose before comparing
Create four copies of the observed source: one with a duplicate A-001, one with an extra fourth field, one without an owner value, and one with a manifest count of 5. Predict a rejected batch in all four cases and no newly created result directory. Explain the distinct reason for each rejection.
For the missing owner case, an empty field and a physically missing field both violate this contract, but they are different parser observations. Your error note should identify the field contract and offending record. Avoid copying every sensitive row into an error log when a record number and safe identity are enough for investigation.
Evidence laboratory — attempt before reading the solution
A synthetic inventory feed contains (asset-1,Desk), (asset-2,Laptop), (asset-1,Desk), (asset-2,Tablet). The contract says one current type per asset; identical duplicate records may be collapsed, but conflicting types must stop publication. The old published report is valid and must remain intact if the new batch fails. Design collection, validation, conflict detection and publication stages. Predict whether this batch can be published.
Official reference for further study
Build a complete, bounded reporting program
Trace the pipeline before running it
The complete program below is saved as reconcile_inventory.py. Its stages are read, validate, index, compare, summarize and export. Collection is outside the program. Remediation is outside the program. Each function owns a clear part of the contract, which makes it possible to test comparison without filesystem access.
The output directory is created exclusively after both inputs have validated. An existing directory causes failure. This avoids silently overwriting yesterday's evidence, and it prevents input validation errors from leaving an apparently useful empty report. Use a new output directory name for each run.
"""Offline, read-only inventory comparison. No endpoint or directory changes."""
import argparse
import csv
import hashlib
import io
import json
import re
import sys
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
FIELDS = ['asset_id', 'owner_id', 'site']
OUTPUT_FIELDS = ['asset_id', 'finding', 'field', 'registered', 'observed']
def parse_capture(value):
if not isinstance(value, str):
raise ValueError('collected_at must be a timezone-aware timestamp string')
stamp = datetime.fromisoformat(value.replace('Z', '+00:00'))
if stamp.tzinfo is None or stamp.utcoffset() is None:
raise ValueError('collected_at must include an offset')
return stamp.astimezone(timezone.utc)
def normalize_row(row, record):
if None in row or any(row.get(key) is None for key in FIELDS):
raise ValueError(f'Record {record}: wrong field count')
asset = row['asset_id'].strip().upper()
owner = row['owner_id'].strip()
site = row['site'].strip().upper()
if not re.fullmatch(r'A-[0-9]{3}', asset):
raise ValueError(f'Record {record}: invalid asset_id')
if not re.fullmatch(r'u[0-9]{3}', owner):
raise ValueError(f'Record {record}: invalid owner_id')
if site not in {'HQ', 'BRANCH'}:
raise ValueError(f'Record {record}: unsupported site')
return {'asset_id': asset, 'owner_id': owner, 'site': site}
def unique_manifest(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError('Manifest has a duplicate property')
result[key] = value
return result
def read_inventory(csv_path, manifest_path):
source_bytes = csv_path.read_bytes()
manifest_bytes = manifest_path.read_bytes()
manifest = json.loads(manifest_bytes.decode('utf-8-sig'), object_pairs_hook=unique_manifest)
if not isinstance(manifest, dict) or set(manifest) != {'complete', 'row_count', 'collected_at'}:
raise ValueError('Manifest must contain complete, row_count and collected_at')
if manifest['complete'] is not True:
raise ValueError('Collector has not declared a complete export')
if type(manifest['row_count']) is not int or manifest['row_count'] < 0:
raise ValueError('Manifest row_count must be a non-negative integer')
capture = parse_capture(manifest['collected_at'])
reader = csv.DictReader(io.StringIO(source_bytes.decode('utf-8-sig'), newline=''), strict=True)
if reader.fieldnames != FIELDS:
raise ValueError('CSV header must be asset_id,owner_id,site in that order')
indexed = {}
for record, raw in enumerate(reader, start=2):
row = normalize_row(raw, record)
asset = row['asset_id']
if asset in indexed:
raise ValueError(f'Record {record}: duplicate normalized identity {asset}')
indexed[asset] = row
if len(indexed) != manifest['row_count']:
raise ValueError('Parsed record count does not match the manifest')
evidence = {
'csv_path': str(csv_path.resolve()),
'manifest_path': str(manifest_path.resolve()),
'csv_sha256': hashlib.sha256(source_bytes).hexdigest(),
'manifest_sha256': hashlib.sha256(manifest_bytes).hexdigest(),
'collected_at': capture.isoformat(),
'row_count': len(indexed),
}
return indexed, evidence
def compare(registered, observed):
findings = []
for asset in sorted(set(registered) | set(observed)):
if asset not in observed:
findings.append({'asset_id': asset, 'finding': 'REGISTER_ONLY', 'field': '', 'registered': 'present', 'observed': 'absent'})
elif asset not in registered:
findings.append({'asset_id': asset, 'finding': 'OBSERVED_ONLY', 'field': '', 'registered': 'absent', 'observed': 'present'})
else:
for field in ['owner_id', 'site']:
if registered[asset][field] != observed[asset][field]:
findings.append({'asset_id': asset, 'finding': 'FIELD_MISMATCH', 'field': field, 'registered': registered[asset][field], 'observed': observed[asset][field]})
return findings
def build_report(register_csv, register_manifest, observed_csv, observed_manifest, output):
registered, register_evidence = read_inventory(register_csv, register_manifest)
observed, observed_evidence = read_inventory(observed_csv, observed_manifest)
findings = compare(registered, observed)
common = set(registered) & set(observed)
same = sum(registered[key] == observed[key] for key in common)
counts = Counter(row['finding'] for row in findings)
summary = {
'schema': 1, 'status': 'complete',
'generated_at': datetime.now(timezone.utc).isoformat(),
'registered': register_evidence, 'observed': observed_evidence,
'registered_assets': len(registered), 'observed_assets': len(observed),
'common_assets': len(common), 'matching_common_assets': same,
'mismatching_common_assets': len(common)-same,
'field_mismatch_rows': counts['FIELD_MISMATCH'],
'register_only_assets': counts['REGISTER_ONLY'],
'observed_only_assets': counts['OBSERVED_ONLY'],
'finding_rows': len(findings),
}
# Exclusive directory creation reserves the result location. If interrupted,
# the directory may remain partial; consumers require a valid final summary.
output.mkdir()
with (output / 'findings.csv').open('x', encoding='utf-8', newline='') as stream:
writer = csv.DictWriter(stream, fieldnames=OUTPUT_FIELDS)
writer.writeheader()
writer.writerows(findings)
summary['findings_sha256'] = hashlib.sha256((output / 'findings.csv').read_bytes()).hexdigest()
with (output / 'summary.json').open('x', encoding='utf-8') as stream:
json.dump(summary, stream, indent=2)
stream.write('\n')
return summary
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('register_csv', type=Path)
parser.add_argument('register_manifest', type=Path)
parser.add_argument('observed_csv', type=Path)
parser.add_argument('observed_manifest', type=Path)
parser.add_argument('output_directory', type=Path)
args = parser.parse_args()
try:
result = build_report(args.register_csv, args.register_manifest, args.observed_csv, args.observed_manifest, args.output_directory)
except (OSError, ValueError, csv.Error) as error:
print(f'Report failed: {error}', file=sys.stderr)
return 2
print(f"Complete comparison: {result['finding_rows']} finding rows; no source changes made")
return 0
if __name__ == '__main__':
raise SystemExit(main())
Run it from the practice directory:
python reconcile_inventory.py register.csv register-manifest.json observed.csv observed-manifest.json result-baseline
A successful run prints four finding rows and exits with status 0. A handled validation or file error prints a diagnostic to standard error and exits with status 2. If your system uses python3 for the selected interpreter, use that command consistently. Keep the output directory name literal for the first run so you can inspect precisely where files were created.
Open result-baseline/findings.csv and result-baseline/summary.json. Compare them with your manual prediction, not just with the program's printed count. The findings must identify two A-002 field mismatches, A-004 as register-only and A-005 as observed-only. No finding should be issued for A-001 or A-003.
Understand the source evidence in the summary
The summary records absolute source paths, hashes of the CSV and manifest bytes, UTC collection timestamps and record counts. An absolute path helps reproduce the input selection within this environment. A hash identifies the exact bytes used. Neither proves who authored the file or that an upstream collector was authorized. A signed export or a controlled collection process would address different provenance questions.
Keep collection time separate from generation time. In the fixture the observed snapshot is five minutes later than the register snapshot. If A-002 changed owner during that interval, the mismatch might be a legitimate timing difference. The report identifies the mismatch; an analyst must evaluate the lifecycle evidence before deciding which value should change.
The summary's schema version helps a consumer reject an incompatible format rather than assume every future field has the same meaning. Names such as field_mismatch_rows and mismatching_common_assets deliberately distinguish output grain. A metric named errors would hide that distinction and encourage inaccurate comparisons.
Design for interrupted writes without claiming atomicity
The first version reserves the output directory and writes the findings before the final summary. If the process crashes or a disk error occurs, a partial directory may remain. Consumers must require a parseable final summary.json with the expected schema and completion status, then verify the expected files, counts and findings-file hash. The hash links the summary to the exported findings bytes; it is not a signed proof of the collector or its authority. A directory's existence is not proof of a successful run.
This is not a fully atomic publication protocol. Another process must not read files while this writer is active, and a robust production design may use a separate staging area, stronger integrity checks and an appropriate atomic publication mechanism. Do not claim that a quick rename is always safe across filesystems or that it always refuses replacement on every operating system.
If a failed attempt leaves a partial result directory, preserve it for diagnosis and use a new path for your next lab attempt. The script will not guess that an existing directory is disposable. This makes reruns inconvenient in a useful way: the learner must acknowledge the evidence instead of accidentally overwriting it.
Repeatability and idempotency are different
Run the same valid inputs with a different result directory. Findings and population counts should be equivalent and ordered consistently. The generation timestamp will differ, and paths can differ if you moved the inputs. Whole-summary byte equality is therefore not the expected test.
The program performs no remote mutation, so repeated comparisons cannot repeatedly change owners or retire assets. That is a bounded side-effect property. It is not proof of exactly-once execution in a scheduler. A later integration that creates tickets needs its own durable identity, duplicate handling and review contract.
Use the words precisely in your project README: the business comparison is deterministic for the same normalized input snapshots; report generation adds run metadata; outputs are not overwritten; source inventories are read-only.
Failure lab with expected outcomes
Change one input at a time and keep the baseline intact. First set the observed manifest to complete:false. Expect status 2, no result directory and no missing-device conclusion. Next duplicate the first asset with a different owner. Expect rejection before comparison; there must not be a “last row wins” report.
Change the observed timestamp to an offset-free string. Expect rejection. Change it instead to 2026-10-01T05:05:00-04:00: the reported UTC collection instant must remain 09:05 UTC. This test distinguishes formatting differences from different instants.
Run again with result-baseline as the output directory. Expect failure and unchanged report bytes. Finally point at a missing CSV path. Expect a diagnostic failure, not four register-only findings. A missing source file and an authoritative empty source are different cases.
For every attempt, record the predicted result, actual exit status, output existence and source hashes before and after. This evidence is more useful than a screenshot saying “script ran.”
Worked laboratory solution
Compare your reasoning with the worked solution
The repeated asset-1/Desk row can collapse under the explicit rule. asset-2 has conflicting types and cannot be resolved by silently keeping whichever row happens to come last. The batch must fail validation and preserve the old report. Write a rejection/conflict report separately, then require a corrected source or documented resolution. For a successful batch, create and validate a temporary output in an appropriate location before committing via the platform-supported replacement approach. Atomic rename behavior depends on filesystem boundaries and does not by itself guarantee every durability property.
Official reference for further study
Workbook already downloaded
Prove the rule and complete an independent project
Execute tests that can disagree with the implementation
Save the following as test_reconcile_inventory.py beside the program. The expected directional and field-mismatch outputs are literal fixture conclusions. They are not calculated by copying the comparison loop into a test helper.
import tempfile
import unittest
from pathlib import Path
import reconcile_inventory as target
class ReconciliationTests(unittest.TestCase):
def test_normalization(self):
got = target.normalize_row({'asset_id':' a-001 ', 'owner_id':'u101', 'site':'hq'},2)
self.assertEqual(got, {'asset_id':'A-001', 'owner_id':'u101', 'site':'HQ'})
def test_two_field_mismatch_is_two_rows_one_asset(self):
reg = {'A-002': {'asset_id':'A-002', 'owner_id':'u102', 'site':'HQ'}}
obs = {'A-002': {'asset_id':'A-002', 'owner_id':'u999', 'site':'BRANCH'}}
self.assertEqual(target.compare(reg, obs), [
{'asset_id':'A-002','finding':'FIELD_MISMATCH','field':'owner_id','registered':'u102','observed':'u999'},
{'asset_id':'A-002','finding':'FIELD_MISMATCH','field':'site','registered':'HQ','observed':'BRANCH'},
])
def test_direction_and_stable_order(self):
same = {'asset_id':'A-001','owner_id':'u101','site':'HQ'}
reg = {'A-004':{}, 'A-001':same}
obs = {'A-005':{}, 'A-001':same}
self.assertEqual(target.compare(reg, obs), [
{'asset_id':'A-004','finding':'REGISTER_ONLY','field':'','registered':'present','observed':'absent'},
{'asset_id':'A-005','finding':'OBSERVED_ONLY','field':'','registered':'absent','observed':'present'},
])
def test_case_sensitive_owner_contract(self):
with self.assertRaises(ValueError):
target.normalize_row({'asset_id':'A-001','owner_id':'U101','site':'HQ'},2)
def test_naive_capture_rejected(self):
with self.assertRaises(ValueError):
target.parse_capture('2026-10-01T09:00:00')
def test_duplicate_after_normalization_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
p = Path(tmp)
(p/'input.csv').write_text('asset_id,owner_id,site\nA-001,u101,HQ\na-001,u101,HQ\n')
(p/'manifest.json').write_text('{"complete":true,"row_count":2,"collected_at":"2026-10-01T09:00:00Z"}')
with self.assertRaisesRegex(ValueError,'duplicate'):
target.read_inventory(p/'input.csv',p/'manifest.json')
if __name__ == '__main__':
unittest.main()
Run python -m unittest -v. All six tests should pass. The file tests create and remove only their own temporary directory. They do not delete the report you created in Chapter 3.
Introduce a deliberate defect in a separate program copy: when comparing sites, replace the observed site value with the registered site value. The two-field mismatch test must fail because the A-002 site mismatch disappears. Restore the original code and rerun. A test that passes the correct program but fails the defective one has demonstrated useful sensitivity.
The tests also distinguish direction, normalization and duplicate identity. They do not test every output-write failure or guarantee performance on millions of rows. Maintain a separate integration test table for file contracts and output preservation. Unit and integration tests answer different questions.
Project A — Add an anomalous population-change review gate
Design a configurable gate that flags an observed population decrease beyond an agreed percentage from a prior successful observed snapshot. It should mark the comparison for review, preserve the current evidence and prevent automated downstream lifecycle actions. It must not invent a missing device's status or modify the source files.
Define the denominator. If a previous observed snapshot had 100 assets and this one has 80, the population decrease is 20% of the previous count. If the previous count is zero, the ordinary percentage is undefined; state a separate policy. Decide whether a threshold of 20% triggers at exactly 20 or only above it, and test that boundary explicitly.
Use independent expected cases: previous 100/current 81, previous 100/current 80, previous 100/current 79, and previous 0/current 4. Include a valid growth case and an incomplete current export. Completeness rejection should occur before the population comparison gate, so an incomplete file cannot be reclassified as an ordinary inventory change.
Worked policy example
A review-at-or-above-20% policy does not flag 100→81, flags 100→80 and 100→79, and uses a separate “no baseline” state for 0→4. An incomplete current export is rejected rather than scored. This policy flags a source population anomaly; it does not establish that individual devices need deletion.
Project B — Accept reordered columns without hiding schema damage
The first version requires one exact header order. Extend it to accept the same three unique names in any order while still rejecting extra, missing and duplicate names. Preserve the output schema and ordering so downstream reports do not change unnecessarily.
A set comparison alone is insufficient: the header asset_id,owner_id,site,site has the same set of names as a valid header but includes a duplicate. Test both header length and unique membership, and validate row shape under the accepted header. Add literal expected cases for a reordered header, an extra header, a duplicate header and a renamed header.
Explain why this is a contract extension rather than a correction of the original report. The original strict order was intentional. Relaxing it can improve interoperability, but it should be a documented, tested versioned behavior change.
Project C — Draft a collector handoff contract
Design the collection stage that supplies these files. You do not need a live tenant to complete the design. Define the expected source population, read identity, permission scope, page-completion rule, timeout behavior, collection timestamp and manifest generation procedure. If collection fails after several pages, specify that the export is incomplete and cannot feed authoritative absence findings.
Record which upstream identifier is stable, whether it is case-sensitive and what duplicate observations mean. If the provider cannot guarantee a snapshot across pages, explain that records may change during collection. A completed page traversal and a transactionally consistent snapshot are different properties.
Connect this project to the REST API course: its page collector can demonstrate bounded pagination and failure handling using offline responses. It will still need an authorized provider adapter and verification before its output is a trustworthy fleet source. Do not remove that boundary merely because the fictional client returns tidy JSON.
Submit an evidence package
Your project folder should include the untouched baseline inputs, both manifests, the program, tests, a successful findings file, its summary and a failure-results table. Add a README with the identity normalization policy, accepted schema, population meaning, output publication limit and chosen extension.
A reviewer should be able to reproduce the baseline and answer these questions from your evidence: why there are four findings but three affected identities; why a duplicate key stops the batch; why changing source formatting changes a hash but may not change findings; why missing files are not empty inventories; and why the report cannot authorize device retirement.
For the extension, provide at least one positive case, one boundary case, one negative case and a defect demonstration. Explain the remaining limitation that matters most for real use. An honest limitation helps the next engineer decide what to verify; it is part of the deliverable rather than a footnote to conceal.
Final reasoning check
A collector declares complete with four rows, but you discover it was scoped to one of three business units. Can the report prove that register-only assets from the other units are absent? No. The program has validated the supplied file contract, not the collector's organization-wide authority.
Two identical normalized rows appear for the same asset. Is silently deduplicating them safe? Not under this course's declared unique-row contract. It would hide an upstream violation. An alternative policy needs explicit justification and tests.
The second report's generation time changes while its findings stay identical. Has deterministic comparison failed? No. Run metadata and comparison results have different contracts. Compare the fields whose stability you promised.
You finish this course by defending the comparison's evidence boundary and demonstrating a tested extension. The website completion check records learning progress; it does not certify a live integration or grant authority to modify an inventory.
References: Python's official CSV, pathlib, argparse and datetime documentation at https://docs.python.org/3/library/ . The runnable code is original course material, not a production collector.
Transfer project — changed conditions
Implement the fixture in a new practice directory. Test repeated identical input, reordered input, conflicting duplicates, malformed records and an injected write failure. Record the old output hash before each failing run and show it remains unchanged. Define exit status and summary counts so a scheduler can distinguish success, validation failure and operational failure. Do not test against production files.
Practical assessment rubric
Score each dimension 0–2: 0 = absent or materially incorrect; 1 = partly correct with an unresolved gap; 2 = correct, explicit and supported by the supplied evidence or an actual labeled lab result.
- Evidence/model: identify the relevant entities, inputs, assumptions and boundaries without inventing facts.
- Main result: derive the case-specific expected result, calculation or decision and show the intermediate reasoning.
- Failure/boundary: test the changed condition and explain why the original conclusion does or does not transfer.
- Verification: supply reproducible steps and expected/observed results; label manual traces as manual, not executed.
- Communication/safety: explain limitations, owners and a proportionate next action without unsafe or unnecessary access.
Target: at least 8/10, with no zero in main result or communication/safety. This is a local learning rubric, not an accredited certification standard. A written/manual solution demonstrates reasoning only; operational competence additionally requires the relevant executable lab in an authorized environment. The native multiple-choice score is separate and does not automatically grade this rubric.
Submission template
- My input/evidence and assumptions:
- Expected result with calculations or trace:
- My implementation/decision and justification:
- Changed-condition result:
- Observed execution, or explicitly labeled manual trace:
- Failure and recovery evidence:
- Unverified limitation and next action:
- Self-score with evidence for each dimension:
Remediation
If the main result is wrong, reconstruct the supplied case from the raw inputs before changing code or controls. If the boundary case is wrong, identify the assumption that changed and rerun both the original and changed case. If verification is missing, do not convert a prediction into an observed result. Reattempt the transfer task without copying the worked solution, then explain it to another person.
Official reference for further study
Assessment: explain your decisions
Choose an answer and write your reason before opening each explanation. Then explain why the other choices do not fit the evidence. Use missed questions to revisit the lesson. This knowledge check is only one part of assessment: complete the practical transfer project as well. It does not predict certification-exam success.
1. Why use DictReader?
- It respects CSV parsing and headers
- It grants permissions
- It automatically validates every number
Answer and explanation
It respects CSV parsing and headers — Parsing and validation are separate responsibilities.
2. What should an empty measurement become?
- A verified zero
- A validation error
- A successful normal device
Answer and explanation
A validation error — Missing evidence is not a zero measurement.
3. What belongs in a repeatable automation design?
- Unbounded append
- No errors
- Explicit output and rerun behavior
Answer and explanation
Explicit output and rerun behavior — Repeated runs must be predictable.
4. A-002 differs in owner and site, while A-004 and A-005 each occur in only one source. What is the result grain?
- Four finding rows concerning three asset identities.
- Four affected assets because there are four findings.
- Two findings because field changes should always be merged.
Answer and explanation
Four finding rows concerning three asset identities. — There are two field rows for A-002 and one presence row for each of A-004 and A-005. Counts must name their grain. The summary separates field rows, mismatching shared assets and directional population differences.
5. The observed collector declares complete:false. Should the program report register-only assets?
- Yes, because a readable CSV is enough.
- No; reject the incomplete export before issuing absence findings.
- Yes, if the number of rows is non-zero.
Answer and explanation
No; reject the incomplete export before issuing absence findings. — Missing records in a partial export cannot establish source absence. Even a complete declaration and matching count do not prove organization-wide authority; the collector scope still requires verification.
6. A-001 and a-001 both appear under the fixture case-insensitive identity contract. What must happen?
- The later row wins because dictionary assignment is convenient.
- Both count as separate assets.
- Reject the duplicate normalized identity instead of guessing a current row.
Answer and explanation
Reject the duplicate normalized identity instead of guessing a current row. — Normalization reveals that both rows refer to one identity. The unique-row contract requires rejection. Choosing a latest row needs revision/timestamp evidence and a separately defined policy.
7. Can the supplied batch be published under its contract?
- No; asset-2 has conflicting types.
- Yes; keep the last row silently.
- Yes; discard all duplicates without explanation.
Answer and explanation
No; asset-2 has conflicting types. — The contract permits identical duplicates but explicitly rejects conflicting values. Input order must not decide an undocumented business rule.
8. What should happen to the old report on validation failure?
- It is replaced with an empty success report.
- It remains intact.
- It is truncated immediately.
Answer and explanation
It remains intact. — The publication boundary must follow successful validation. Preserving the last valid output prevents a failed run from destroying usable data.
9. What best demonstrates repeatability here?
- The script prints “done.”
- Every exception is suppressed.
- Reruns and reordered input produce the same validated result under the documented rules.
Answer and explanation
Reruns and reordered input produce the same validated result under the documented rules. — A repeatability claim needs observable output and failure tests, not a success message.
Use the explained questions above for offline practice. A scored attempt requires the course assessment on CogniSkill.ca.