What you'll learn
Control Flow taught us to process records. Now we'll understand the records themselves: how text is cleaned, how ordered collections work, and how named fields become the building blocks of inventory reports and future automation.
- Distinguish strings, lists, dictionaries, tuples and sets by the problem they solve.
- Access list items by index, slice safely and recognize mutable versus immutable values.
- Read and update dictionary records using keys, get() and predictable missing-value defaults.
- Transform and filter inventory records with loops without overwriting the original data.
- Handle split(), strip(), join(), enumerate(), sorted() and basic type conversion.
- Build a small report from structured device records and check edge cases.
- Translate a familiar for loop into a short list comprehension, then decide which is easier to read.
- Copy, sort, remove and update list or dictionary values without corrupting the source.
Strings: inspect, clean and combine
An IP address, hostname or ticket message starts as text. Strings are sequences of characters: you can index or slice them, but not change one character in place. Normalize incoming text before comparing it. strip removes surrounding whitespace; lower creates a lowercase copy; split divides a string into a list using a delimiter. Keep the original when you need an audit trail.
Immutable values
name[0] reads a character, but name[0] = 'S' raises TypeError. Build a new string instead of trying to mutate one character.
Normalize explicitly
strip() and lower() return new strings. ' Offline '.strip().lower() equals 'offline', but the original text remains untouched.
raw = ' SW-EDGE-01, Offline '
parts = raw.strip().split(',')
host = parts[0].lower()
state = parts[1].strip().lower()
print(host, state)Does split remove the whitespace after the comma automatically? Reveal explanation after you predict.
No. The second field initially contains a leading space. Applying strip() to that field removes it; the original string is unchanged.
Lists: positions, slices and edits
A Python list holds an ordered collection of items. Lists are mutable, so append(), assignment and pop() modify the same object. Indices start at zero; -1 refers to the last element. A slice such as items[1:3] includes index 1 but excludes index 3. A shallow list slice copies its outer container, not nested objects.
Index safely
ports[0] is the first port; ports[-1] is last. Reading a missing index raises IndexError—check length when the position is uncertain.
Mutation is visible
append() modifies the list and returns None. Assign the original list to another name and both names refer to the same object unless you copy it.
ports = [22, 80, 443]
ports.append(3389)
print(ports[0])
print(ports[1:3])
print(len(ports))What does ports[1:3] print, and does the original list change? Reveal explanation after you predict.
It prints [80, 443]. The slice itself does not change ports, but append added 3389 to the original list, which now has four elements.
Copy, sort and remove list entries deliberately
A list can change in place, unlike a string. Understand the difference between assigning another name to the same list and creating a shallow copy: alias = ports refers to the same list, while copy = ports.copy() creates an independent outer list. The sort() method changes a list and returns None; sorted(ports) builds a new sorted list. pop(index) removes and returns the entry at that index, so a careless pop can shift all later positions. Try these operations on fictional port lists before updating any real configuration.
Alias versus copy
alias = ports means both names point to the same list. copied = ports.copy() creates a separate outer container. Shallow copies still share nested objects: copying a list of dictionaries does not copy the dictionaries.
sort() versus sorted()
ports.sort() reorders ports and returns None. sorted(ports) leaves the original list in its existing order and produces a new sorted list. Choose based on whether input order must be retained.
pop(), remove() and membership
pop(1) removes and returns the item at index 1. remove(443) removes the first occurrence of value 443, raising ValueError when absent. Test membership or know the input contract before removing.
ports = [443, 22, 3389, 80]
copy = ports.copy()
removed = copy.pop(2)
copy.sort()
print(ports)
print(copy)
print(removed)Which list changes, what does removed contain, and what would print(copy.sort()) produce? Reveal explanation after you predict.
ports remains [443, 22, 3389, 80]. copy becomes [22, 80, 443], and removed is 3389. copy.sort() mutates and returns None, so printing that call would print None.
Does the original list change?
Choose an operation on a fictional port list and compare its before-and-after state. The actual Python methods are explained in the lesson; this explorer models their effects.
[443, 3389, 22, 80][443,3389,22,80][443,22,80]clean = ports.copy()
clean.pop(1)The copy has a separate outer list. Removing an item from clean does not change ports.
This example contains integers, so a shallow copy fully separates these outer list changes. Nested dictionaries would still be shared between shallow copies.
Dictionaries: model a device record
A list tracks items by position; a dictionary tracks values by unique hashable keys. This is an excellent fit for inventory records with hostname, role and status. A missing key raises KeyError when accessed with brackets. Use get(key, default) when a field is optional and that default is explicitly meaningful; do not silently pretend missing evidence is healthy.
One record, many attributes
Use descriptive keys like 'hostname' and 'status'. Add or replace a field with record['status'] = 'online'.
Missing is not healthy
record.get('status', 'unknown') surfaces a missing status as unknown rather than inventing an online state.
device = {'hostname': 'SW-01', 'status': 'offline'}
print(device['hostname'])
print(device.get('site', 'unknown'))
device['status'] = 'online'
print(device['status'])What happens if you use device['site'] rather than get('site', 'unknown')? Reveal explanation after you predict.
Python raises KeyError because site does not exist in the dictionary. get provides the explicit fallback 'unknown'.
Dictionary updates, iteration and safe removal
A dictionary stores keys and values rather than numeric positions. You can add a new field with record['site'] = 'HQ', replace a value with update(), and remove a key with pop(key, default) or del. Without a default, pop raises KeyError if the key does not exist. When reading a dictionary, items() yields key/value pairs and keys() yields keys; iteration over a dictionary directly also visits its keys. Copies are shallow, just like list copies, so protect the original inventory when reporting.
Updates and copies
new_record = record.copy() makes a new outer dictionary. new_record.update({'status':'offline'}) changes the copy, not the original. Nested lists or dictionaries remain shared unless copied at deeper levels.
Safe deletion
record.pop('retired_field', None) removes the key when present and avoids KeyError when absent. Use del record['field'] only if the key must exist.
Iterate keys and values
for key, value in record.items(): visits each pair. Use get(key, default) for optional fields; the default should represent unknown rather than assuming a healthy value.
original = {'host': 'SW-01', 'status': 'online', 'legacy': True}
review = original.copy()
review.update({'status': 'offline', 'site': 'HQ'})
review.pop('legacy', None)
print(original['status'])
print(sorted(review.items()))Does original['status'] change? Which fields remain in the new review copy? Reveal explanation after you predict.
original['status'] is still online. The new copy contains host SW-01, site HQ and status offline. legacy was removed only from the copied record.
Combine lists and dictionaries
An IT inventory often consists of a list of dictionaries. Each dictionary describes one device and the list holds all the records. Loop through those records and access fields by name. When data is inconsistent, normalize status carefully. The sample is fictional and does not connect to live systems.
Separate each record
Each device is its own dictionary within one list, making hostnames and statuses easy to associate.
Loop through records
for device in devices lets you read each device['hostname']. Use device.get('status', 'unknown') to avoid assuming all sources are complete.
devices = [
{'hostname': 'SW-01', 'status': 'online'},
{'hostname': 'AP-02', 'status': 'offline'},
]
for device in devices:
print(device['hostname'], device['status'])How many dictionary records does this list hold, and how many lines print? Reveal explanation after you predict.
Two records and two printed lines. The outer list provides ordering, and each dictionary provides meaningful field names.
Change records. Watch the report.
Try a filter, toggle sorting, or change a device's status. This is a simulation of how a list of dictionaries is processed—not a real network connection.
| Hostname | Site | Status (editable) |
|---|---|---|
SW-01 | HQ | |
AP-02 | BR1 | |
FW-01 | HQ | |
SRV-01 | BR2 |
[
{
"host": "AP-02",
"site": "BR1",
"status": "offline"
},
{
"host": "FW-01",
"site": "HQ",
"status": "offline"
},
{
"host": "SRV-01",
"site": "BR2",
"status": "unknown"
},
{
"host": "SW-01",
"site": "HQ",
"status": "online"
}
]The filtered output is a new collection. Changing the filter does not delete or rewrite the source records.
Filter and count matching records
Real operations reports answer questions such as which devices are offline. Begin with an empty results list, inspect every record, append matches, and report after the loop. Avoid accidentally assigning the original list to your results; this would make your edits affect the input. Normalize status for case and whitespace differences before comparing.
Keep the source intact
Build a fresh offline_hosts list rather than deleting records while iterating.
Report exact matches
Normalize with str(...).strip().lower() when the source format warrants it. Distinguish 'unknown' from 'offline'.
devices = [
{'host': 'SW-01', 'status': 'Online'},
{'host': 'AP-02', 'status': ' offline '},
]
offline_hosts = []
for device in devices:
if device['status'].strip().lower() == 'offline':
offline_hosts.append(device['host'])
print(offline_hosts)Why doesn't the first device appear in the final results? Reveal explanation after you predict.
Its normalized status is online rather than offline. Only AP-02 matches the condition and gets appended to the new list.
Tuples and sets: fixed groups and uniqueness
Tuples are ordered immutable sequences; they are useful for values you want to keep grouped without changing their positions. Sets store unique hashable elements and support membership checks, but they do not preserve an indexable sequence. Converting a list to a set removes duplicates, and printing a set is not guaranteed to use a stable order. Sort for deterministic output.
Tuple means fixed structure
('SW-01', 22) can be unpacked into hostname, port. You cannot append to a tuple, though nested mutable objects can still change.
Set means unique members
Use set() to count distinct sites. Sort before displaying values when a stable user-facing report matters.
sites = ['HQ', 'BR1', 'HQ', 'BR2']
unique_sites = set(sites)
print(len(unique_sites))
print(sorted(unique_sites))Will set(sites) contain two copies of HQ? Reveal explanation after you predict.
No. Sets contain unique members, so this example has three distinct sites. sorted() produces the predictable order ['BR1', 'BR2', 'HQ'].
List comprehensions: understand the long way first
You already know how to start with an empty list, loop over records, test a condition and append matches. A list comprehension is a shorter expression for that same common pattern. Read the version with a for loop first, then compare it with [expression for item in collection if condition]. Both produce a new list without removing the original records. You do not have to prefer the shorter syntax if the expanded loop is easier to debug.
Left side: what to collect
In [device['host'] for device in devices if ...], device['host'] is the value stored in the new list. It is not an index into the resulting list.
Right side: repeat and filter
for device in devices visits each record, while if device['status'] == 'offline' selects which ones are included. Leave the if out when every item belongs in the result.
devices = [{'host':'SW-01','status':'online'},
{'host':'AP-02','status':'offline'}]
slow_way = []
for device in devices:
if device['status'] == 'offline':
slow_way.append(device['host'])
short_way = [device['host'] for device in devices
if device['status'] == 'offline']
print(slow_way == short_way)
print(short_way)Do both methods create the same new list? What would happen if no records were offline? Reveal explanation after you predict.
Yes. Both create ['AP-02'], and the comparison prints True. If no record matched, each approach would produce an empty list rather than an error. Use the expanded loop when it communicates the steps more clearly.
Transform, sort and format
An inventory may arrive as a delimited text line. split() separates fields; strip() cleans them; int() converts a numeric-looking string to an integer and can raise ValueError for bad input. enumerate() supplies a counter plus the current item as you loop, usually starting at zero. For example, for position, hostname in enumerate(['SW-01', 'AP-02'], start=1): prints positions 1 and 2 when you print(position, hostname). sorted() returns a sorted new list. join() combines a sequence of strings into a readable report; nonstrings must be converted first.
Conversions can fail
int('443') becomes 443, but int('443/tcp') raises ValueError. Validate formats and add error handling as a later skill.
Output contract matters
Use sorted(hosts) for a consistent report and ', '.join(hosts) to combine known strings without accidental list-representation formatting.
raw = 'SW-01,443,online'
name, port_text, status = [part.strip() for part in raw.split(',')]
port = int(port_text)
print(f'{name}: {port} / {status}')Which operation turns '443' from text into a number? Reveal explanation after you predict.
int(port_text) converts the string '443' to integer 443. This conversion can fail for malformed input, so input validation matters.
Create a small device health report
A useful first automation reads a collection, classifies its records, and prints a reliable summary. Keep the original inventory available, store intermediate results separately, then report counts and hostnames. Test zero failures, several failures and missing or malformed statuses. This prepares you for CSV, JSON, file handling and API requests later in the Python path.
Inspect before changing
Count records with len(), then filter. Print the final results outside the loop to avoid premature or repeated summaries.
Design for missing fields
get('status', 'unknown') avoids a crash but also keeps unknown distinct from offline. Never count unknown as healthy by default.
inventory = [
{'host': 'SW-01', 'status': 'online'},
{'host': 'AP-02', 'status': 'offline'},
{'host': 'FW-01', 'status': 'offline'},
]
offline = [d['host'] for d in inventory if d.get('status') == 'offline']
print(f"{len(offline)} offline: {', '.join(sorted(offline))}")What should the report say and why should sorted be used? Reveal explanation after you predict.
It should say '2 offline: AP-02, FW-01'. Sorting makes the report deterministic if the input order changes.
Make a real data transformation
Use the same real in-browser Python environment from earlier lessons. Start by normalizing a messy status string, then continue to eight more data-handling challenges, including copying, sorting, dictionary updates and tuple unpacking, in Practice.
Write it. Run it. Understand it.
Clean, combine and report on real Python collections. Write a transformation and verify it.
Normalize a device status
The fictitious inventory returned a whitespace-padded mixed-case status. Normalize it to lowercase 'offline', store the result in clean_status and print exactly 'offline'. Do not replace the raw value itself.
offlineTry solving it before asking for help. Hints never show a completed program.
Your printed output and errors will appear here.
Run the code, then step through the executed lines to watch variable values change.
From reading collections to processing them.
Practice cleaning strings, editing lists, handling missing dictionary keys, filtering records, finding unique sites and building a small health report.
Continue to all Working with Data challenges →Reference material
Python's official tutorial explains these types in greater detail, including behaviors you'll revisit during file handling and automation.
