Cloud Resource Tag Validator: Part 1
Build a Python CLI that checks simulated cloud resource tags for policy issues.
Introduction
30 Second Summary
Labels only help when everyone spells them the same way. A blank value or stray capital letter can turn an orderly inventory into a guessing game.
In this project, you will build a terminal-based Cloud Resource Tag Validator in Python. The tool checks simulated cloud-resource tags against a policy while handling ordinary input mistakes safely.
What You'll Build
Your terminal will show a Cloud Resource Tag Validator turning a chosen resource count into explained compliance results plus a final summary.
By the end of this project, you'll have:
- Run a compliance check against four simulated cloud resources.
- Read specific explanations for missing, empty, incorrectly capitalized, or invalid tags.
- Recover from invalid input without crashing the program.
- Secret Mission: Detect two tag keys that differ only by capitalization before they create ambiguous governance data.
Are there any prerequisites?
You need no programming experience because your computer already has Python plus Visual Studio Code with its Python extension. This local first part does not use your AWS account.
Before We Start
Before you begin, take a moment to connect the Python skills you are about to learn with the cloud-governance problem your local tool solves. This gives every concept a clear purpose before you connect the same ideas to Linux and AWS in later projects.
Confirm the Interpreter and Run the First Script
Your tag validator begins as text in a file. Visual Studio Code edits that file.
The selected Python interpreter reads the saved script. It executes each top-level statement.
Confirming that relationship prevents setup failures from looking like code failures. You will prove the path with one print() statement.
In this step, get ready to:
- Prepare the cloud-tag-validator workspace in Visual Studio Code.
- Confirm a Python 3 interpreter.
- Run tag_validator.py from Visual Studio Code.
Confirm the runtime Visual Studio Code will use
Visual Studio Code stores your project files. The selected interpreter supplies the Python runtime that executes them.
- Press the Windows key to open the Windows search bar.
- Type Visual Studio Code into the search bar.
- Press Enter to open Visual Studio Code.
- Click File in the top menu.
- Click Open Folder.
- Browse to your Desktop in the folder picker.
- Use the folder creation control to create cloud-tag-validator.
- Select cloud-tag-validator to open it in Visual Studio Code.
What if Visual Studio Code asks about trust?
Visual Studio Code may ask whether you trust the folder. Confirm that you trust it because you created the folder yourself.
- Use the new-file control in the left sidebar to create tag_validator.py inside cloud-tag-validator.
- Read the Python interpreter shown in the Visual Studio Code Status Bar.
- Press Ctrl+Shift+P if the Status Bar does not show Python 3.
- Enter Python: Select Interpreter in the Command Palette.
- Select an interpreter labeled Python 3.
The Status Bar now identifies a Python 3 interpreter. Visual Studio Code uses that interpreter for running your script.
- Open a fresh terminal panel from Visual Studio Code's top menu.
Before you run the check, which Python version family do you expect the terminal to report? Keep that prediction in mind.
- Confirm the terminal runtime by running this command:
python --version
What Does This Command Prove?
The --version option asks Python to print its version number before exiting.
Output beginning with Python 3 confirms that the terminal can reach a Python 3 runtime.
You should see a Python version beginning with Python 3 in the terminal.
Don't See Python 3?
- Return to the interpreter shown in the Visual Studio Code Status Bar.
- Select a Python 3 interpreter with Python: Select Interpreter.
- Open a fresh terminal panel before repeating the version check.
Ask for help with the interpreter selection.
Run the smallest complete script
A Python script can contain top-level statements that execute in source order. A single print() call gives you a visible result to trace from the editor to the terminal.
- Add the setup message to tag_validator.py by replacing the empty file with this code:
print("Cloud Tag Validator is ready.")
What Does This Code Do?
The print() function writes the supplied text to the terminal. The quotation marks make the message a string value.
This one statement is enough to prove that the interpreter can read your saved file.
- Save tag_validator.py.
Before you run the file, what message do you expect the interpreter to print? Keep that prediction in mind.
- Select Run Python File in Terminal from the editor toolbar.
You should see Cloud Tag Validator is ready. in the terminal.
Don't See the Setup Message?
- Save tag_validator.py before running it again.
- Select the tag_validator.py editor tab before using Run Python File in Terminal.
- Check that the line contains matching quotation marks and parentheses.
Ask for help tracing the missing output.
A second top-level statement lets you test the interpreter's execution order. Each statement runs when the interpreter reaches its line.
- Add a second message by replacing the contents of tag_validator.py with this code:
print("Cloud Tag Validator is ready.")
print("Cloud Resource Tag Validator")
Why Is the Order Predictable?
The interpreter reaches the setup message first. It reaches the project title on the next line.
The source order determines the terminal output order.
- Save tag_validator.py.
Before you run the file, which message do you expect to appear first? Keep that prediction in mind.
- Select Run Python File in Terminal from the editor toolbar.
You should see Cloud Tag Validator is ready. first. The line Cloud Resource Tag Validator appears second.
Is the Latest Output Hard to Find?
- Look at the final two output lines in the terminal.
- Compare those lines with the current order in tag_validator.py.
Ask for help reading the execution order.
Reversing the statements tests the same execution rule with a different source order. The terminal result should follow the edited file.
- Reverse the two statements by replacing the contents of tag_validator.py with this code:
print("Cloud Resource Tag Validator")
print("Cloud Tag Validator is ready.")
What Changed?
The two statements contain the same messages. Their new source order changes which message the interpreter reaches first.
- Save tag_validator.py.
Before you run the reversed file, which message should move to the first output line? Keep that prediction in mind.
- Select Run Python File in Terminal from the editor toolbar.
You should see Cloud Resource Tag Validator first. The setup message now appears second.
Did the Output Stay the Same?
- Confirm that you saved the reversed statements in tag_validator.py.
- Run the active tag_validator.py editor tab again.
Ask for help checking why the saved order is missing.
The finished artifact for this step keeps one setup message. Restoring that line leaves a clean starting point for the one-resource check.
- Restore the final step artifact by replacing the contents of tag_validator.py with this code:
print("Cloud Tag Validator is ready.")
Why Restore One Message?
The two-message experiment proved that top-level statements execute in sequence. The single setup message becomes the exact starting file for the next step.
- Save tag_validator.py.
Before the final run, what single line should the terminal show? Keep that prediction in mind.
- Select Run Python File in Terminal from the editor toolbar.
You should see exactly one setup message: Cloud Tag Validator is ready.
Still Seeing Two Messages?
- Confirm that tag_validator.py contains one print() statement.
- Save the file before using Run Python File in Terminal again.
- Read the latest output at the bottom of the terminal.
Ask for help restoring the final script.
That confirms the full path from saved file to terminal output. Your editor and interpreter are ready for the validator logic.
✔️ Awesome, I've got everything!
Great. Double check that tag_validator.py is saved with one setup message.
ⓧ I'd like to double check the full code
print("Cloud Tag Validator is ready.")
Your first Python script now runs through the selected interpreter. Next, you will replace the setup message with a one-resource tag check.
Build a One-Resource Tag Check
Your first script proved that Visual Studio Code can pass tag_validator.py to the selected Python interpreter. The terminal showed the result of that execution.
A useful tag check needs named values plus rules that explain each result. Keeping the check to one resource makes every value easy to trace before collections add more moving parts.
In this step, get ready to:
- Represent one simulated cloud resource with variables.
- Inspect string types plus a Boolean type.
- Clean the environment value before producing a compliance result.
Represent one resource with named values
A variable binds a name to a value that the interpreter can reuse. This resource needs separate names for its identity plus each required tag value.
- Replace the setup print statement in tag_validator.py with the code below:
resource_id = "compute-001"
owner = "cloud-team"
environment = " production "
cost_center = "CC100"
required_values_present = bool(owner and environment and cost_center)
print(resource_id)
print(type(owner))
print(type(required_values_present))
What Does This Code Do?
- The first four variables hold the simulated resource data.
- The and expression checks whether all three required tag values contain something.
- The bool() call stores that result as either true or false.
- The type() calls reveal the kind of object bound to each inspected name.
- Save tag_validator.py.
- Predict the order in which the resource ID plus the two types appear.
- Select Run Python File in Terminal from the editor's top-right toolbar.
You'll see compute-001 first. The following lines identify a string type plus a Boolean type.
Good start. Your simulated resource now has named values that Python can inspect.
Still Seeing the Setup Message?
Confirm that you replaced the original print() line. Save tag_validator.py before running it again.
Check that the active editor tab is tag_validator.py before selecting Run Python File in Terminal.
Help me check why my Python file still prints the previous setup message.
Quotation marks create a text value. An unquoted word makes Python search for a matching name.
- Find the line that assigns cloud-team to owner.
- Remove the quotation marks around cloud-team.
- Save tag_validator.py.
- Predict whether Python can resolve the unquoted value as a name.
- Select Run Python File in Terminal.
The traceback ends with NameError. This intentional failure shows that the interpreter searched for a name that your script never bound.
- Put quotation marks back around cloud-team.
- Save tag_validator.py.
- Predict whether the three output lines return.
- Select Run Python File in Terminal.
You'll see the resource ID plus both types again. Restoring the string lets Python finish every top-level statement.
Clean strings and make a decision
Tag values can contain extra whitespace or inconsistent capitalization. Cleaning the environment value gives the policy one predictable form to compare.
- Replace the current variable plus print section in tag_validator.py with the code below:
resource_id = "compute-001"
owner = "cloud-team"
environment = " production "
cost_center = "CC100"
required_values_present = bool(owner and environment and cost_center)
cleaned_environment = environment.strip()
normalized_environment = cleaned_environment.lower()
print(resource_id)
print(cleaned_environment)
print(type(owner))
print(type(required_values_present))
How Does the Cleanup Work?
- The strip() method removes the whitespace surrounding production.
- The lower() method creates a lowercase value for policy comparison.
- The original environment value remains available because each cleanup result is assigned to a new variable.
- Save tag_validator.py.
- Predict how " production " appears after strip() processes it.
- Select Run Python File in Terminal.
You'll see production without surrounding spaces. That confirms your string-cleanup path is working.
Still Seeing Extra Spaces?
Check that the second print statement uses cleaned_environment. Printing environment shows the original value.
Help me find why strip is not removing the surrounding spaces from my environment value.
A conditional chain checks rules in order. The first matching branch prints a specific explanation while the final branch confirms compliance.
- Append the decision code below to the end of tag_validator.py:
if not required_values_present:
print("NONCOMPLIANT: Required tag values are missing.")
elif normalized_environment not in {"development", "test", "production"}:
print("NONCOMPLIANT: Environment is not allowed.")
else:
print("COMPLIANT")
How Does the Decision Work?
- The if branch catches an empty required value first.
- The elif branch checks whether the normalized environment belongs to the allowed set.
- The else branch runs when both policy checks pass.
- Four spaces place each print() call inside its branch.
- Save tag_validator.py.
- Predict which branch handles the current resource.
- Select Run Python File in Terminal.
You'll see the resource ID plus the cleaned environment. The final line is COMPLIANT because every required value is present.
Your first complete policy decision works. Now each test can reveal exactly which Boolean expression controls the result.
- Replace "cloud-team" with an empty string written as "".
- Save tag_validator.py.
- Predict which Boolean expression becomes false.
- Select Run Python File in Terminal.
You'll see NONCOMPLIANT: Required tag values are missing. because the empty owner makes required_values_present false.
- Replace the empty owner with "cloud-team".
- Save tag_validator.py.
- Predict whether the compliant result returns.
- Select Run Python File in Terminal.
You'll see COMPLIANT again. The restored owner passes the required-values check.
- Replace " production " with "sandbox".
- Save tag_validator.py.
- Predict which membership expression becomes false.
- Select Run Python File in Terminal.
You'll see NONCOMPLIANT: Environment is not allowed. because sandbox is absent from the allowed set.
- Replace "sandbox" with " production ".
- Save tag_validator.py.
- Predict the final five output lines before this last check.
- Select Run Python File in Terminal.
You'll see compute-001 plus the cleaned value production. The type lines follow before the final COMPLIANT result.
Decision Result Look Wrong?
Check that normalized_environment appears in the membership test. Using the original value keeps its surrounding spaces.
Confirm that each branch body has four spaces of indentation. Misaligned lines can stop Python before it reaches the decision.
Help me troubleshoot the conditional result in my one-resource Python tag check.
✔️ Awesome, I've got everything!
Your saved tag_validator.py now contains the complete one-resource check.
ⓧ I'd like to double check the full code
resource_id = "compute-001"
owner = "cloud-team"
environment = " production "
cost_center = "CC100"
required_values_present = bool(owner and environment and cost_center)
cleaned_environment = environment.strip()
normalized_environment = cleaned_environment.lower()
print(resource_id)
print(cleaned_environment)
print(type(owner))
print(type(required_values_present))
if not required_values_present:
print("NONCOMPLIANT: Required tag values are missing.")
elif normalized_environment not in {"development", "test", "production"}:
print("NONCOMPLIANT: Environment is not allowed.")
else:
print("COMPLIANT")
Your one-resource check now cleans input before explaining its decision. Next, you'll apply this reasoning to a batch of simulated resources.
Model Multiple Resources and Reproduce a KeyError
Your one-resource check now produces a compliance result. That is your first working policy decision.
A realistic batch needs each resource to carry its own metadata. Python collections give each part a structure that matches its job.
Nested dictionaries make every lookup depend on an exact key. In this step, you will let the runtime test that assumption.
In this step, get ready to:
- Model fixed policy keys and allowed values with collections.
- Store four simulated resources in an ordered batch.
- Run an exact dictionary lookup and interpret the resulting traceback.
Choose collections by meaning
The policy contains fixed required tag names and a unique group of allowed environments. A tuple protects the fixed sequence while a set stores distinct allowed values.
Each resource is a dictionary because its keys map to values. A list keeps the four resource dictionaries in inspection order.
The resource list spans two editing chunks. The first chunk ends at a resource boundary. The second chunk closes the list before you run it.
- Replace all existing code in the tag_validator.py editor tab with the first half of the collection model below:
REQUIRED_TAGS = ("Owner", "Environment", "CostCenter")
ALLOWED_ENVIRONMENTS = {"development", "test", "production"}
RESOURCES = [
{
"resource_id": "compute-001",
"resource_type": "compute",
"tags": {
"Owner": "cloud-team",
"Environment": "production",
"CostCenter": "CC100",
},
},
{
"resource_id": "storage-002",
"resource_type": "storage",
"tags": {
"Owner": " ",
"environment": "test",
"CostCenter": "CC200",
},
},
Why use these collections?
- The REQUIRED_TAGS tuple holds policy names that remain fixed.
- The ALLOWED_ENVIRONMENTS set keeps each allowed value unique.
- The open RESOURCES list preserves the order used during inspection.
- Each nested tags dictionary maps a tag key to its value.
- Place your cursor after the final comma in tag_validator.py.
- Complete the collection model with the remaining resources and type checks below:
{
"resource_id": "database-003",
"resource_type": "database",
"tags": {
"Owner": "data-team",
"Environment": "Test",
"CostCenter": "CC300",
},
},
{
"resource_id": "network-004",
"resource_type": "network",
"tags": {
"Owner": "network-team",
"Environment": "sandbox",
"CostCenter": "CC400",
},
},
]
print(type(REQUIRED_TAGS))
print(type(ALLOWED_ENVIRONMENTS))
print(type(RESOURCES))
What do the type checks prove?
- The first type check confirms that the required tag names form an immutable tuple.
- The second type check confirms that the allowed environments form a unique-value set.
- The final type check confirms that the resources form an ordered list.
- Save tag_validator.py.
- Select Run Python File in Terminal in the editor toolbar.
You should see three type lines identifying a tuple, a set, and a list in that order. Your single-resource variables now form a four-resource data model.
Do the collection types look different?
Check that the closing square bracket appears before the three print() calls. A missing bracket prevents Python from finishing the resource list.
Confirm that REQUIRED_TAGS uses parentheses. Confirm that ALLOWED_ENVIRONMENTS uses curly braces.
Help me check my Python collection types.
Reproduce the unsafe lookup
Dictionary subscription retrieves a value through one exact key. The next loop trusts every resource to contain that key with the same capitalization.
- Move to the bottom of tag_validator.py.
- Add the direct environment lookup below the three type checks:
for resource in RESOURCES:
environment = resource["tags"]["Environment"]
print(resource["resource_id"], environment)
What does this loop test?
- The for loop visits resources in list order.
- The nested subscription enters the tags dictionary and requests the exact Environment key.
- The final line prints each resource identifier only after its environment lookup succeeds.
Before the run, consider whether all four resources use the exact key this lookup expects.
- Save tag_validator.py.
- Select Run Python File in Terminal in the editor toolbar.
You will see the three collection types followed by the result for compute-001. Execution then stops with a traceback ending in KeyError.
Why did the lookup stop?
The first resource passed, so the loop advanced to storage-002. Its tag dictionary contains the lowercase key environment.
The lookup requests Environment with an uppercase first letter. Dictionary keys are exact, so Python cannot retrieve that entry.
- Read the final traceback line to identify the missing key.
- Move upward to find the highlighted dictionary lookup in tag_validator.py.
- Use the resource order to identify storage-002 as the resource after the printed compute-001 result.
Did the script finish without KeyError?
Check that the storage-002 dictionary still uses lowercase environment. The loop must request uppercase Environment.
Confirm that you ran the saved tag_validator.py file from the cloud-tag-validator folder.
Help me reproduce the intended KeyError.
✔️ Awesome, I've got everything!
You have reproduced the controlled lookup failure. That traceback proves an exact-key assumption can stop the entire resource batch.
ⓧ I'd like to double check the full code
REQUIRED_TAGS = ("Owner", "Environment", "CostCenter")
ALLOWED_ENVIRONMENTS = {"development", "test", "production"}
RESOURCES = [
{
"resource_id": "compute-001",
"resource_type": "compute",
"tags": {
"Owner": "cloud-team",
"Environment": "production",
"CostCenter": "CC100",
},
},
{
"resource_id": "storage-002",
"resource_type": "storage",
"tags": {
"Owner": " ",
"environment": "test",
"CostCenter": "CC200",
},
},
{
"resource_id": "database-003",
"resource_type": "database",
"tags": {
"Owner": "data-team",
"Environment": "Test",
"CostCenter": "CC300",
},
},
{
"resource_id": "network-004",
"resource_type": "network",
"tags": {
"Owner": "network-team",
"Environment": "sandbox",
"CostCenter": "CC400",
},
},
]
print(type(REQUIRED_TAGS))
print(type(ALLOWED_ENVIRONMENTS))
print(type(RESOURCES))
for resource in RESOURCES:
environment = resource["tags"]["Environment"]
print(resource["resource_id"], environment)
Your data model now resembles a batch of cloud resource responses. Next, you will replace the brittle lookup with checks that explain every problem without stopping the report.
Validate Tags Without Crashing
Your last run exposed the risk inside a direct dictionary lookup. The script reached storage-002 before stopping with a KeyError.
This step makes the Python validator check each key before retrieval. It also collects every failure so one imperfect resource cannot stop the batch.
In this step, get ready to:
- Detect missing keys and capitalization mismatches before retrieving values.
- Validate tag values while collecting clear issue explanations.
- Process all four resources without triggering a KeyError.
Check presence before retrieval
A dictionary subscription assumes that its exact key exists. Membership checks let the validator identify a missing key before continue moves to the next required tag.
- In tag_validator.py, select the three type-printing lines plus the unsafe loop beneath RESOURCES.
- Replace the selected code with these safe key-checking functions:
def find_case_mismatch(tags, expected_key):
for actual_key in tags:
same_letters = actual_key.lower() == expected_key.lower()
different_spelling = actual_key != expected_key
if same_letters and different_spelling:
return actual_key
return ""
def validate_resource(resource):
issues = []
tags = resource["tags"]
for required_tag in REQUIRED_TAGS:
if required_tag not in tags:
mismatched_key = find_case_mismatch(tags, required_tag)
if mismatched_key:
issues.append("Tag key '{0}' must be capitalized as '{1}'.".format(mismatched_key, required_tag))
else:
issues.append("Missing tag: {0}.".format(required_tag))
continue
return issues
What does this code do?
- The find_case_mismatch() helper compares lowercase versions of the actual key and the required key.
- The validate_resource() function creates an issues list for one resource.
- The continue statement prevents retrieval when a required key is absent.
- Connect the validator to the resource batch by adding this code below the functions:
for resource in RESOURCES:
resource_issues = validate_resource(resource)
print(resource["resource_id"], resource_issues)
How Does the Batch Use the Validator?
- The loop passes one resource at a time into validate_resource().
- The returned resource_issues list keeps each explanation beside its resource ID.
- Save tag_validator.py.
Before you run it, do you expect the batch to stop at storage-002 again?
- Select Run Python File in Terminal from the play menu in the editor toolbar.
You will see all four resource IDs in the terminal. The storage-002 result identifies that environment must be capitalized as Environment.
You have removed the crash from the main path. The validator can now explain a key mismatch before continuing to the next resource.
Still Seeing a KeyError?
Check that the old environment = resource["tags"]["Environment"] line no longer exists beneath RESOURCES.
Confirm that the batch calls validate_resource(resource) before printing resource_issues.
Help me trace why my safe dictionary validation still raises KeyError.
A missing key follows the same safe path as a capitalization mismatch. You can prove that branch works with a temporary change.
- Delete the "CostCenter": "CC400", line from the network-004 resource.
- Save tag_validator.py.
Before you run the file, which branch do you think handles the missing CostCenter key?
- Select Run Python File in Terminal from the play menu in the editor toolbar.
The network-004 result now includes Missing tag: CostCenter. The loop still reaches the end of the batch.
- Restore the "CostCenter": "CC400", line inside network-004.
- Save tag_validator.py.
Before you rerun the file, which resource should lose its missing-tag explanation?
- Select Run Python File in Terminal from the play menu in the editor toolbar.
The missing CostCenter explanation disappears from network-004. Your original four-resource data is restored.
Validate values and collect explanations
String methods such as strip() require a string value. An isinstance() check protects those method calls while issues.append() preserves each failure.
- Inside validate_resource(), find the continue statement in the required-tag loop.
- Insert this value-checking block after that missing-key branch:
tag_value = tags[required_tag]
if not isinstance(tag_value, str):
issues.append("Tag {0} must contain text.".format(required_tag))
elif not tag_value.strip():
issues.append("Tag {0} cannot be empty.".format(required_tag))
What Does the Value Check Protect?
- The isinstance() check catches values that cannot safely use string methods.
- The strip() check treats a string containing only spaces as empty.
- Each failed rule adds a specific explanation to issues.
- Save tag_validator.py.
Before you run the file, which existing resource contains a tag value made entirely of spaces?
- Select Run Python File in Terminal from the play menu in the editor toolbar.
The storage-002 result now includes Tag Owner cannot be empty. The capitalization explanation remains in the same issues list.
Missing the Empty-Owner Explanation?
Confirm that tag_value = tags[required_tag] remains indented inside the for required_tag in REQUIRED_TAGS loop.
Check that the elif line uses not tag_value.strip().
Help me debug why my validator does not identify the whitespace-only Owner tag.
- Change the database-003 cost center value from "CC300" to 300.
- Save tag_validator.py.
Before you run the file, do you expect conversion or type validation to handle the integer?
- Select Run Python File in Terminal from the play menu in the editor toolbar.
The database-003 result includes Tag CostCenter must contain text. The type check prevents the validator from calling strip() on the integer.
- Restore the database-003 cost center value to "CC300".
- Save tag_validator.py.
- Select Run Python File in Terminal to confirm the CostCenter type explanation is gone.
Environment validation needs both the original spelling and a normalized copy. The original supports a useful report while the lowercase copy supports membership testing.
- Inside validate_resource(), find the final return issues line.
- Insert this environment-validation block immediately before that return line:
environment = tags.get("Environment")
if isinstance(environment, str) and environment.strip():
cleaned_environment = environment.strip()
normalized_environment = cleaned_environment.lower()
if normalized_environment not in ALLOWED_ENVIRONMENTS:
issues.append("Environment '{0}' is not allowed.".format(cleaned_environment))
elif cleaned_environment != normalized_environment:
issues.append("Environment '{0}' must be lowercase.".format(cleaned_environment))
How Does Environment Validation Work?
- The get() call safely returns an empty result when the exact Environment key is absent.
- The cleaned_environment value removes surrounding whitespace for a useful report.
- The normalized_environment value supports a membership check against ALLOWED_ENVIRONMENTS.
- The nested conditions distinguish an unsupported value from an incorrectly capitalized value.
- Save tag_validator.py.
Before you run the final check, which resources do you expect to fail because of an environment value?
- Select Run Python File in Terminal from the play menu in the editor toolbar.
What Should You See?
- The compute-001 result contains an empty issues list.
- The storage-002 result reports a mis-capitalized key plus an empty owner.
- The database-003 result reports that Test must be lowercase.
- The network-004 result reports that sandbox is not allowed.
That is the full batch running safely. Every resource reaches the terminal with a specific compliance explanation.
Report Missing an Environment Issue?
Check that the environment block sits outside the required-tag loop. Its lines use four spaces of indentation inside validate_resource().
Confirm that return issues remains below the complete environment block.
Help me debug why my validator does not report lowercase or unsupported Environment values.
✔️ Awesome, I've got everything!
Great. Double check that you saved tag_validator.py after the successful four-resource run.
ⓧ I'd like to double check the full code
REQUIRED_TAGS = ("Owner", "Environment", "CostCenter")
ALLOWED_ENVIRONMENTS = {"development", "test", "production"}
RESOURCES = [
{
"resource_id": "compute-001",
"resource_type": "compute",
"tags": {
"Owner": "cloud-team",
"Environment": "production",
"CostCenter": "CC100",
},
},
{
"resource_id": "storage-002",
"resource_type": "storage",
"tags": {
"Owner": " ",
"environment": "test",
"CostCenter": "CC200",
},
},
{
"resource_id": "database-003",
"resource_type": "database",
"tags": {
"Owner": "data-team",
"Environment": "Test",
"CostCenter": "CC300",
},
},
{
"resource_id": "network-004",
"resource_type": "network",
"tags": {
"Owner": "network-team",
"Environment": "sandbox",
"CostCenter": "CC400",
},
},
]
def find_case_mismatch(tags, expected_key):
for actual_key in tags:
same_letters = actual_key.lower() == expected_key.lower()
different_spelling = actual_key != expected_key
if same_letters and different_spelling:
return actual_key
return ""
def validate_resource(resource):
issues = []
tags = resource["tags"]
for required_tag in REQUIRED_TAGS:
if required_tag not in tags:
mismatched_key = find_case_mismatch(tags, required_tag)
if mismatched_key:
issues.append("Tag key '{0}' must be capitalized as '{1}'.".format(mismatched_key, required_tag))
else:
issues.append("Missing tag: {0}.".format(required_tag))
continue
tag_value = tags[required_tag]
if not isinstance(tag_value, str):
issues.append("Tag {0} must contain text.".format(required_tag))
elif not tag_value.strip():
issues.append("Tag {0} cannot be empty.".format(required_tag))
environment = tags.get("Environment")
if isinstance(environment, str) and environment.strip():
cleaned_environment = environment.strip()
normalized_environment = cleaned_environment.lower()
if normalized_environment not in ALLOWED_ENVIRONMENTS:
issues.append("Environment '{0}' is not allowed.".format(cleaned_environment))
elif cleaned_environment != normalized_environment:
issues.append("Environment '{0}' must be lowercase.".format(cleaned_environment))
return issues
for resource in RESOURCES:
resource_issues = validate_resource(resource)
print(resource["resource_id"], resource_issues)
Your validator now handles imperfect tag data without losing the rest of the batch. Next up, you will refactor these rules into an interactive report with reusable functions and validated terminal input.
Refactor Into an Interactive Tool
Your Python validator now checks all four resources without crashing. It also explains every failed tagging rule.
The working loop still mixes report formatting with program control. Small functions separate those responsibilities. Targeted exception handling also lets the finished tool recover from invalid terminal input.
In this step, get ready to:
- Organize report formatting inside a reusable function.
- Convert terminal input while handling invalid values.
- Connect the functions through a clear entry point.
Separate report formatting from validation
The validator already returns an issue list for one resource. A dedicated report function can reuse that result for every resource while keeping output formatting in one place.
Variables created inside the function have local scope. The caller only needs the returned noncompliant count.
- In tag_validator.py, place your cursor immediately above the existing for resource in RESOURCES: loop.
- Insert the new print_report() function by copying the code below:
def print_report(resources):
"""Print each result and return the number of noncompliant resources."""
noncompliant_count = 0
for resource in resources:
issues = validate_resource(resource)
resource_id = resource["resource_id"]
resource_type = resource["resource_type"]
if not issues:
print("{0} ({1}): COMPLIANT".format(resource_id, resource_type))
else:
noncompliant_count = noncompliant_count + 1
print("{0} ({1}): NONCOMPLIANT".format(resource_id, resource_type))
for issue in issues:
print(" - {0}".format(issue))
return noncompliant_count
What does this function do?
- The resources parameter receives the batch that needs a report.
- The noncompliant_count variable records how many resources have at least one issue.
- The nested loop prints each explanation beneath its noncompliant resource.
- The return statement sends the final count back to the caller.
- Save tag_validator.py.
- Select Run Python File in Terminal from the editor's run menu.
You should still see the four resource IDs with their issue lists. This confirms the new function contains valid Python while the existing loop preserves the current output.
Seeing a syntax or indentation error?
Place print_report() above the existing bottom loop. Keep every line in the function indented with four spaces.
Check that the nested for issue in issues: loop sits inside the else block.
Help me fix the indentation in my print_report function.
Convert and validate terminal input
Terminal input arrives as a string. The program needs to convert that string into an integer before it can select a number of resources.
A while loop keeps the prompt active until the value is usable. Catching only ValueError handles the expected conversion failure while leaving unrelated errors visible.
- Place your cursor immediately above print_report().
- Add ask_resource_limit() by copying the code below:
def ask_resource_limit(total_resources):
"""Keep asking until the learner enters an integer in the valid range."""
while True:
raw_limit = input(
"How many resources should be checked (1-{0})? ".format(
total_resources
)
).strip()
try:
resource_limit = int(raw_limit)
except ValueError:
print("Enter a whole number, not text.")
continue
if 1 <= resource_limit <= total_resources:
return resource_limit
print("Choose a number from 1 to {0}.".format(total_resources))
How does the input loop work?
- The input() call displays the prompt and returns what the learner entered as a string.
- The int() conversion raises ValueError when that string does not represent a whole number.
- The continue statement starts the next loop iteration after invalid text.
- The range check returns only a value from 1 through the number of available resources.
- Save tag_validator.py.
- Select Run Python File in Terminal from the editor's run menu.
You should still see the four list-style results from the existing bottom loop. The successful run confirms that Python can define the new input function.
Does the script stop before printing resources?
Check that ask_resource_limit() appears above print_report(). Make sure the except ValueError: line aligns with try:.
Help me fix my resource limit function.
The final controller decides when to ask for input. It also selects the requested resources before sending them to the report function.
- Find the existing for resource in RESOURCES: loop at the bottom of tag_validator.py.
- Replace that bottom loop with main() plus the entry-point check below:
def main():
"""Control the program's input, validation, and final summary."""
print("Cloud Resource Tag Validator")
print("=" * 28)
resource_limit = ask_resource_limit(len(RESOURCES))
selected_resources = RESOURCES[:resource_limit]
noncompliant_count = print_report(selected_resources)
print()
print(
"Checked {0} resource(s). Noncompliant: {1}.".format(
len(selected_resources), noncompliant_count
)
)
if __name__ == "__main__":
main()
How does the controller connect everything?
- The main() function controls input and reporting without containing either implementation.
- The len() call supplies the number of available resources to the input function.
- The selected_resources slice contains only the requested number of dictionaries.
- The __name__ == "__main__" check starts main() when Python executes this file as the script.
- Save tag_validator.py.
Before you run the script, predict whether the word cloud reaches the report or starts another input attempt.
- Select Run Python File in Terminal from the editor's run menu.
- Enter cloud at the resource prompt.
You should see Enter a whole number, not text. The same resource prompt follows because continue starts another loop iteration.
- Enter 4 at the repeated prompt.
You should see a readable result for all four resources. The final line should be Checked 4 resource(s). Noncompliant: 3..
Does invalid text crash the script?
Check that resource_limit = int(raw_limit) sits inside the try block. Confirm that the handler names only ValueError.
Check that continue is indented inside the exception handler. That placement sends invalid text back to the prompt.
Help me stop invalid text from crashing my validator.
Finish the portfolio-facing report
A readable project should explain intent at the points where a future reader needs context. Module comments describe the shared policy data. Function docstrings summarize each responsibility.
- Compare tag_validator.py with the complete version in the second tab below.
- Reconcile every difference so the comments and function docstrings match the complete version.
✔️ Awesome, I've got everything!
Great. Confirm that tag_validator.py is saved with all five functions and the entry-point check.
ⓧ I'd like to double check the full code
# Fixed policy data is kept at module scope so every validation function can read it.
REQUIRED_TAGS = ("Owner", "Environment", "CostCenter")
ALLOWED_ENVIRONMENTS = {"development", "test", "production"}
# These dictionaries simulate resource data that a later project can load from JSON.
RESOURCES = [
{
"resource_id": "compute-001",
"resource_type": "compute",
"tags": {
"Owner": "cloud-team",
"Environment": "production",
"CostCenter": "CC100",
},
},
{
"resource_id": "storage-002",
"resource_type": "storage",
"tags": {
"Owner": " ",
"environment": "test",
"CostCenter": "CC200",
},
},
{
"resource_id": "database-003",
"resource_type": "database",
"tags": {
"Owner": "data-team",
"Environment": "Test",
"CostCenter": "CC300",
},
},
{
"resource_id": "network-004",
"resource_type": "network",
"tags": {
"Owner": "network-team",
"Environment": "sandbox",
"CostCenter": "CC400",
},
},
]
def find_case_mismatch(tags, expected_key):
"""Return the actual key when only its capitalization is wrong."""
for actual_key in tags:
same_letters = actual_key.lower() == expected_key.lower()
different_spelling = actual_key != expected_key
if same_letters and different_spelling:
return actual_key
return ""
def validate_resource(resource):
"""Return a list of explanations for every failed tag rule."""
issues = []
tags = resource["tags"]
for required_tag in REQUIRED_TAGS:
if required_tag not in tags:
mismatched_key = find_case_mismatch(tags, required_tag)
if mismatched_key:
issues.append(
"Tag key '{0}' must be capitalized as '{1}'.".format(
mismatched_key, required_tag
)
)
else:
issues.append("Missing tag: {0}.".format(required_tag))
continue
tag_value = tags[required_tag]
if not isinstance(tag_value, str):
issues.append("Tag {0} must contain text.".format(required_tag))
elif not tag_value.strip():
issues.append("Tag {0} cannot be empty.".format(required_tag))
environment = tags.get("Environment")
if isinstance(environment, str) and environment.strip():
cleaned_environment = environment.strip()
normalized_environment = cleaned_environment.lower()
if normalized_environment not in ALLOWED_ENVIRONMENTS:
issues.append(
"Environment '{0}' is not allowed.".format(cleaned_environment)
)
elif cleaned_environment != normalized_environment:
issues.append(
"Environment '{0}' must be lowercase.".format(cleaned_environment)
)
return issues
def ask_resource_limit(total_resources):
"""Keep asking until the learner enters an integer in the valid range."""
while True:
raw_limit = input(
"How many resources should be checked (1-{0})? ".format(
total_resources
)
).strip()
try:
resource_limit = int(raw_limit)
except ValueError:
print("Enter a whole number, not text.")
continue
if 1 <= resource_limit <= total_resources:
return resource_limit
print("Choose a number from 1 to {0}.".format(total_resources))
def print_report(resources):
"""Print each result and return the number of noncompliant resources."""
noncompliant_count = 0
for resource in resources:
issues = validate_resource(resource)
resource_id = resource["resource_id"]
resource_type = resource["resource_type"]
if not issues:
print("{0} ({1}): COMPLIANT".format(resource_id, resource_type))
else:
noncompliant_count = noncompliant_count + 1
print("{0} ({1}): NONCOMPLIANT".format(resource_id, resource_type))
for issue in issues:
print(" - {0}".format(issue))
return noncompliant_count
def main():
"""Control the program's input, validation, and final summary."""
print("Cloud Resource Tag Validator")
print("=" * 28)
resource_limit = ask_resource_limit(len(RESOURCES))
selected_resources = RESOURCES[:resource_limit]
noncompliant_count = print_report(selected_resources)
print()
print(
"Checked {0} resource(s). Noncompliant: {1}.".format(
len(selected_resources), noncompliant_count
)
)
if __name__ == "__main__":
main()
A local variable can change names without affecting its caller when every reference inside that function changes with it. This is the practical benefit of local scope.
- Temporarily rename resource_id inside print_report() to a temporary name of your choice.
- Update the two references to that local name inside print_report().
- Save tag_validator.py.
Before you run the script, predict whether renaming that local variable changes the report text.
- Select Run Python File in Terminal from the editor's run menu.
- Enter 4 at the resource prompt.
You should see the same four resource results and the same final count. The caller never depends on the local variable's spelling.
- Undo the temporary local-variable rename.
- Save tag_validator.py.
The final check exercises conversion failure and range validation in one run. Predict which inputs reach int() successfully before you begin.
- Select Run Python File in Terminal from the editor's run menu.
- Enter cloud at the first prompt.
You should see Enter a whole number, not text. The program should ask again without displaying a traceback.
- Enter 0 at the repeated prompt.
You should see Choose a number from 1 to 4. The conversion succeeded before the range check rejected the value.
- Enter 5 at the repeated prompt.
You should see the same range message because only four simulated resources are available.
- Enter 4 at the repeated prompt.
You should see compute-001 marked COMPLIANT. The other three resources should be marked NONCOMPLIANT with indented explanations.
That is the refactor complete. Your validator now survives ordinary input mistakes and turns four resource dictionaries into a readable compliance report.
Secret mission
Detect Duplicate Keys with Different Capitalization
A resource can contain two tag keys that differ only by capitalization. In this mission, you will detect that ambiguity while preserving both original spellings in the compliance report.
Clean Up Your Resources
Clean Up Your Resources
Your validator stays on your Windows computer. It creates no AWS resources and carries no ongoing costs.
Decide whether to keep the local files, pause your work, or delete the project folder.
Resources you used:
- The cloud-tag-validator folder containing tag_validator.py.
- The integrated terminal session in Visual Studio Code, if tag_validator.py is still running.
Keep everything running
No cleanup is needed. Choose this if you want to continue testing your tag validator.
- Save tag_validator.py.
- Keep the cloud-tag-validator folder in its current location.
- Select Run Python File in Terminal whenever you want to test another resource count.
Pause - I'll come back to this later
End any active run to free the integrated terminal. Your project files remain ready for another session.
- Save tag_validator.py.
- Use the terminal panel's stop control if tag_validator.py is waiting for input.
- Close Visual Studio Code.
- Return to the cloud-tag-validator folder in Visual Studio Code when you are ready to resume.
- Select Run Python File in Terminal to run the validator again.
You'll see Cloud Resource Tag Validator followed by the resource-count prompt.
Delete - I don't want to use this again
Remove the local project if you do not want to reuse it. Your existing Python and Visual Studio Code installations stay available.
- Use the terminal panel's stop control if tag_validator.py is waiting for input.
- Close Visual Studio Code.
- Delete the cloud-tag-validator folder from its current location using the Windows file browser.
You should no longer see the cloud-tag-validator folder in that location. Your local project is now removed.
Nice Work!
Nice Work!
You did it! You've built a local Python validator for simulated cloud resources. It explains why each tag set passes or fails your policy.
You've learned how to:
- Trace Python execution from a saved script to terminal output. Use branching to explain a resource's compliance result.
- Model cloud tag data with purpose-built collections. Replace a crashing dictionary lookup with safe validation.
- Build an interactive report from reusable functions. Handle invalid input with a targeted exception.
- Secret Mission: Detect duplicate tag keys that differ only by capitalization. Preserve both original spellings in each warning.
Ready to quiz yourself?