Part-6: Talking to Linux with Python
Build a Python system snapshot that captures, parses, and saves Linux data.
Introduction
30 Second Summary
When a computer runs low on space or starts feeling slow, the useful clues are scattered across separate command results. Repeating those checks by hand makes each result easy to miss or forget.
In this project, you will build a small system snapshot that uses Python to collect live disk, memory, and uptime information from Linux. The finished snapshot prints a friendly summary in the standard Linux terminal before saving the same results as JSON.
What You'll Build
Running one Python script produces a friendly system summary in the standard Linux terminal plus a structured system_snapshot.json file that you can inspect in a standard Linux text editor (like nano, vim, or gedit).
By the end of this project, you'll have:
- A reusable command runner that starts Linux commands as separate subprocesses while recording their output, errors, and return codes.
- A live system summary that turns disk, memory, and uptime command output into named values you can read at a glance.
- A failure-safe JSON report that remains valid when a command reports a problem or cannot start.
- Secret Mission: Add a project-defined warning that changes when root filesystem usage reaches 80 percent.
Are there any prerequisites?
You should already understand Python variables, collections, conditions, loops, functions, exceptions, imports, and text or JSON files.
You also need basic Linux terminal experience with the tools already installed in a standard Linux environment.
Before We Start
This checkpoint locks in the purpose of your system snapshot before you write any code. The snapshot collects disk, memory, and uptime facts as structured JSON for later inspection.
Run Your First Linux Command
A Linux command answers one system question at a time. Putting that command in Python turns the check into repeatable automation.
This step creates the first version of script.py. You will run uptime from Python. You will also test how command arguments work with ls and df -h.
In this step, get ready to:
- Confirm that your installed Python version supports the subprocess features used later.
- Create script.py in a standard Linux text editor (like nano, vim, or gedit).
- Test ls, df -h, and uptime through Python command argument lists.
Prepare the project file
Later lessons use capture_output and text to collect command results. These options require Python 3.7 or newer.
- Use the application launcher in a standard Linux environment to search for a standard Linux text editor (like nano, vim, or gedit).
- Select your standard Linux text editor from the search results.
- Use the application launcher to search for the standard Linux terminal.
- Select the standard Linux terminal from the search results.
- Keep the text editor open so you can create and edit script.py.
- Confirm that the editor saves files as plain text.
- Switch to the standard Linux terminal.
- Confirm the installed Python version by running this command:
python3 --version
What Does This Check Prove?
The command reports the installed Python 3 version. Output beginning with Python 3 confirms that python3 is available.
A version of 3.7 or newer supports the subprocess options used later.
You should see a version beginning with Python 3. The version number should be 3.7 or newer.
- Create a new plain text file in your standard Linux text editor.
- Open the editor's save option.
- Choose the folder shown in the standard Linux terminal.
- Enter script.py as the file name.
- Confirm that the file uses plain text format.
- Save the file.
Your standard Linux text editor now shows script.py as the saved file. The folder containing it is your project folder.
Run uptime from Python
Python as a Remote Control
A remote control asks another device to perform an action. Python can ask Linux to start a command while your script remains in control.
- A process is a running program.
- A subprocess is a separate process started by another program.
- A command argument is a value passed to a command to control its behaviour.
💡 Why Use subprocess.run()?
Python recommends subprocess.run() for subprocess tasks it can handle. The function starts a command before waiting for it to finish.
An argument sequence keeps the program name separate from each option. This project does not need a shell between Python and the Linux command.
- Add the first command runner to script.py by entering this code:
import subprocess
subprocess.run(["uptime"])
What Does This Code Do?
- The import subprocess line loads Python's process-launching tools.
- The subprocess.run() call starts uptime as a separate process.
- The one-item list identifies the command that Python starts.
- Save script.py in your standard Linux text editor.
Before you run the script, predict which program prints the visible uptime line:
- Switch to the standard Linux terminal.
- Run the saved script with this command:
python3 script.py
What Should You See?
The visible line comes from uptime. Python starts that command before waiting for its result.
You should see one machine-specific line shaped like <current time> up <machine-specific duration>, <users>, load average: <machine-specific values>. You should see no traceback.
🙋♀️ Seeing the Previous Result?
Forgetting to save script.py leaves Python running the previous file contents. Save the file in your standard Linux text editor before rerunning it.
Help me work out why my saved Python script still shows the previous result.
Practice command arguments
The list passed to subprocess.run() places the program name first. Command options occupy their own positions after it.
Exercise 1: List files. This swap proves that the same Python call can start another Linux command.
- In your standard Linux text editor, find subprocess.run(["uptime"]).
- Replace uptime with ls.
- Save script.py.
- Run the modified script in the standard Linux terminal with this command:
python3 script.py
What Should You See?
You should see machine-specific file names from the project folder. The output includes script.py.
Exercise 2: Inspect disk space. This command needs an option that asks for human-readable sizes.
- In your standard Linux text editor, find the argument list passed to subprocess.run().
- Replace the current argument list with ['df', '-h'].
- Save script.py.
- Run the modified script in the standard Linux terminal with this command:
python3 script.py
What Should You See?
You should see a filesystem table. Its device names and storage values are machine-specific.
The -h option displays sizes with powers of 1024 and size letters.
🙋♀️ Did the Disk Command Fail to Start?
Passing "df -h" as one program name makes Python search for an executable with that complete name. Use ['df', '-h'] so the command and its option occupy separate list items.
Help me check the argument list in my subprocess call.
Exercise 3: Restore uptime. The final script keeps the command in a one-item argument list.
- In your standard Linux text editor, replace the current subprocess.run() call with this line:
subprocess.run(["uptime"])
What Does This Line Restore?
The list now contains only uptime. Python starts that command without additional arguments.
- Save script.py.
Before the final check, predict whether the restored one-item list produces a table or one uptime line:
- Run the final script in the standard Linux terminal with this command:
python3 script.py
What Does the Final Run Prove?
You should see one machine-specific uptime line. You should see no traceback.
The result proves that Python can start a Linux process from a saved script.
✔️ Awesome, I've got everything!
Your saved file is ready. Keep the standard Linux terminal open in the same project folder.
ⓧ I'd like to double check the full code
import subprocess
subprocess.run(["uptime"])
This complete file imports the subprocess module before running uptime through a one-item argument list.
Your Python-to-Linux connection now works. Next, you will capture command output so the script can clean and reuse it.
Python can start a Linux command as a separate process and wait for its result.
Capture Disk and Memory Output
Your Python script can already launch Linux commands through subprocess.run(). The command currently sends its reply straight to the terminal.
A DevOps snapshot needs to reuse command replies after each process finishes. Captured output lets your script store those replies for later processing.
You will capture disk output as text. You will repeat the pattern with memory output before testing whether a process lookup provides enough information on its own.
In this step, get ready to:
- Capture a machine-specific disk table as reusable text.
- Compare memory output with the separate error stream.
- Test whether captured output alone can explain a process lookup.
Capture the disk report
How captured output works
Picture a mailbox with one slot for regular replies plus another slot for problems.
- stdout is the stream where a command sends its normal output.
- stderr is the separate stream where a command can send error information.
- Capture means asking Python to keep those streams instead of displaying them immediately.
- Text mode tells Python to provide each captured stream as an ordinary string.
- In your standard Linux text editor, find subprocess.run(["uptime"]).
- Replace that line with the code below:
disk_result = subprocess.run(["df", "-h"], capture_output=True, text=True)
print("DISK")
print(disk_result.stdout.strip())
What does this code do?
- subprocess.run() starts df with -h as a separate command argument.
- capture_output=True stores the command's normal output plus its error output.
- text=True provides both captured streams as strings.
- disk_result holds the returned CompletedProcess object.
- strip() removes whitespace from the beginning plus the end of the captured disk table.
- Save script.py.
Before you run it, make a quick prediction about whether the captured disk table will match the earlier terminal output.
- Switch to the standard Linux terminal.
- Run the updated script with this command:
python3 script.py
What should you see?
You should see DISK followed by a filesystem table. Its device names plus storage values reflect your machine's current state.
🙋♀️ Mistake 1: stdout is None
The stdout value is None when output was not captured. Confirm that the call includes capture_output=True.
Help me fix captured stdout in this script.
Capture memory replies
The first exercise applies the same capture pattern to memory information. Separate result variables keep each command's reply available under a clear name.
- In your standard Linux text editor, replace everything below import subprocess with the code below:
disk_result = subprocess.run(["df", "-h"], capture_output=True, text=True)
memory_result = subprocess.run(["free", "-m"], capture_output=True, text=True)
print("DISK")
print(disk_result.stdout.strip())
print("MEMORY")
print(memory_result.stdout.strip())
How does the memory capture work?
- memory_result stores the result from free -m.
- memory_result.stdout contains the machine-specific memory table.
- MEMORY gives the captured table a clear terminal label.
- Save script.py.
- Switch to the standard Linux terminal.
- Run the memory exercise with this command:
python3 script.py
What should you see?
You should see the DISK label with a filesystem table. Below it, you should see the MEMORY label with a machine-specific memory table.
🙋♀️ Mistake 2: output is bytes
Captured streams remain bytes when text mode is missing. Confirm that both calls include text=True.
Help me turn captured bytes into strings.
The second exercise makes an empty error stream visible. A successful command usually has no error text to report.
- In script.py, temporarily replace print(disk_result.stdout.strip()) with print(repr(disk_result.stderr)).
- Save script.py.
- Switch to the standard Linux terminal.
- Run the stderr exercise with this command:
python3 script.py
What does the empty result mean?
You should see '' below the DISK label. The repr() call displays the empty error string with quotes so you can see that the value exists.
Test a process lookup
The third exercise captures a process lookup in the same way. This check reveals whether normal output gives enough evidence to judge the result.
- In your standard Linux text editor, replace everything below import subprocess with the final code below:
disk_result = subprocess.run(["df", "-h"], capture_output=True, text=True)
memory_result = subprocess.run(["free", "-m"], capture_output=True, text=True)
process_result = subprocess.run(
["pgrep", "-x", "no_such_process_x"],
capture_output=True,
text=True,
)
print("DISK")
print(disk_result.stdout.strip())
print("MEMORY")
print(memory_result.stdout.strip())
print("PROCESS LOOKUP")
print(process_result.stdout.strip())
What does the process capture add?
- process_result stores the result from the process lookup.
- -x asks pgrep to match the process name exactly.
- process_result.stdout.strip() prints only the cleaned normal output from that lookup.
- Save script.py.
Before you run it, predict whether the process lookup label will contain a useful reply.
- Switch to the standard Linux terminal.
- Run the final exercise with this command:
python3 script.py
What should you see?
You should see DISK plus a machine-specific filesystem table. You should then see MEMORY plus a machine-specific memory table.
The PROCESS LOOKUP label is followed by a blank value. The blank is the intended shortfall.
The exact-match pattern has 17 characters. Process-name matching for pgrep is limited to 15 characters.
Your script only prints stdout, so the blank reply gives no friendly status. The next step uses the return code plus stderr to explain what happened.
✔️ Awesome, I've got everything!
Great. Double-check that you saved script.py with all three captured command results.
ⓧ I'd like to double check the full code
import subprocess
disk_result = subprocess.run(["df", "-h"], capture_output=True, text=True)
memory_result = subprocess.run(["free", "-m"], capture_output=True, text=True)
process_result = subprocess.run(
["pgrep", "-x", "no_such_process_x"],
capture_output=True,
text=True,
)
print("DISK")
print(disk_result.stdout.strip())
print("MEMORY")
print(memory_result.stdout.strip())
print("PROCESS LOOKUP")
print(process_result.stdout.strip())
Capturing gives Python separate text strings for a command's normal output and error output.
Good work. Your script now holds disk, memory, plus process replies as reusable text.
Next, you will read return codes plus stderr to turn the silent process lookup into a friendly status.
Handle Command Results
Your process lookup from earlier printed a blank line. Empty stdout cannot distinguish a missing match from a command that failed to start.
Each subprocess also produces a return code. The return code reveals whether the command reported success. Python raises OSError when the operating system cannot start a command.
In this step, get ready to:
- Use return codes to understand command outcomes.
- Build a reusable function that records each command result.
- Turn the blank process lookup into a friendly status.
Revisit the blank process lookup
The current script shows captured output without checking the command's status. This recreates the silent shortfall that return codes need to solve.
Do you expect the blank lookup to explain why no process matched? Hold that prediction while you run the check.
- Run the current version of script.py by running:
python3 script.py
What Does This Check Do?
This command asks Python to execute script.py. The current script prints only the cleaned output from each Linux command.
You see the DISK table. You also see the MEMORY table. The space after PROCESS LOOKUP remains blank.
What Is a Return Code?
A return code works like a delivery receipt. It is an integer that reports the outcome after a command finishes.
A code of 0 typically means success. A nonzero code means the command reported another outcome or a failure.
An OSError is an exception raised when the operating system cannot start the requested command.
Build a reusable command runner
A reusable runner gives every command the same result shape. Your later code can inspect named fields without repeating the subprocess setup.
- Switch back to script.py in your standard Linux text editor.
- Place run_command() below the import by pasting this code:
def run_command(command):
try:
result = subprocess.run(command, capture_output=True, text=True)
except OSError as error:
return {
"command": command,
"ok": False,
"returncode": None,
"stdout": "",
"stderr": str(error),
}
return {
"command": command,
"ok": result.returncode == 0,
"returncode": result.returncode,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
}
What Does This Function Do?
- The try block asks the operating system to start the command.
- The except OSError block records a command that could not start.
- The ok field becomes True only when the return code is 0.
- The stdout field stores cleaned normal output.
- The stderr field stores cleaned error output.
- Save script.py.
- Confirm the existing experiment still runs by running:
python3 script.py
What Does This Run Prove?
Python reads the new function definition before continuing into the existing experiment. A clean run proves the function has valid structure.
You still see the disk table. You still see the memory table. The process lookup remains blank because the existing code has not called run_command() yet.
Seeing a Traceback After Adding the Function?
Check that the code inside try is indented by eight spaces. Check that both return statements remain inside run_command().
Check each dictionary entry for its closing comma.
Help me fix the structure of my run_command function.
💡 Trace Three Command Outcomes
- A successful uptime result normally stores 0 in returncode.
- The exact process lookup stores 1 because no process matches.
- A command that cannot start enters the except OSError branch. Its result stores None as the return code.
Handle the failed lookup
The exact process lookup uses return code 1 to report that no process matched. Your script can now translate that status into a message a person can understand.
- Select everything below the run_command() function in script.py.
- Replace the selected code by pasting this block:
process_result = run_command(["pgrep", "-x", "no_such_process_x"])
if process_result["returncode"] == 1:
print("Process not found")
else:
print(process_result["stdout"])
print(f"Return code: {process_result['returncode']}")
What Does This Code Do?
- The script passes the command argument list into run_command().
- The if condition recognizes the documented no-match return code.
- The friendly message replaces the unexplained blank output.
- The final line exposes the return code for verification.
- Save script.py.
Which two lines would prove the failed lookup is now understood? Make your prediction before the final run.
- Run the updated script by running:
python3 script.py
What Does the Final Run Prove?
This run exercises the reusable function with the intentionally unsuccessful lookup. The printed status proves your script interprets the return code without crashing.
In the standard Linux terminal, you see Process not found on the first line. You see Return code: 1 on the next line.
That closes the blind spot from the blank lookup. Your script now reports what the command outcome means.
Still Getting the Wrong Status?
- Mistake: Deciding success from non-empty stdout. Fix: Use returncode because successful commands can produce no normal output.
- Mistake: Handling nonzero return codes without catching startup failures. Fix: Keep the subprocess call inside try so OSError becomes a result dictionary.
Help me diagnose an unexpected command result.
The finished file should now match one complete version.
✔️ Awesome, I've got everything!
Your script now has the reusable runner and the friendly process status. Double-check that you saved script.py.
ⓧ I'd like to double check the full code
import subprocess
def run_command(command):
try:
result = subprocess.run(command, capture_output=True, text=True)
except OSError as error:
return {
"command": command,
"ok": False,
"returncode": None,
"stdout": "",
"stderr": str(error),
}
return {
"command": command,
"ok": result.returncode == 0,
"returncode": result.returncode,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
}
process_result = run_command(["pgrep", "-x", "no_such_process_x"])
if process_result["returncode"] == 1:
print("Process not found")
else:
print(process_result["stdout"])
print(f"Return code: {process_result['returncode']}")
A reliable command runner checks both whether the command started and which return code it produced. Next, you will turn captured command text into named values that Python can reuse.
Parse System Information
Your reusable command runner can now distinguish a successful command from one that failed. It also preserves the command's cleaned output for the next stage.
Multi-line terminal text is difficult to reuse in a system report. In this step, you will parse selected disk and memory fields into dictionaries with named values.
In this step, get ready to:
- Split command output into lines and fields.
- Extract root filesystem details into a dictionary.
- Extract memory details into a dictionary.
Read command output as rows and fields
Parsing turns command text into values that your program can use. A field is one piece of information within a row.
How does the receipt pattern work?
A grocery receipt contains rows of items. Each row contains fields such as a name or price.
- Use splitlines() to turn multi-line text into a list of rows.
- Use split() to separate one row wherever runs of whitespace appear.
- Use a loop to inspect rows until you find the one you need.
- Use a dictionary to store each selected field under a descriptive key.
- Return to the standard Linux terminal from earlier.
- Display the live memory table by running this command:
free -m
Exercise 1: Count the Memory Rows
Count the lines in the table. That count matches the number of entries that splitlines() creates from this output.
The numeric values describe your machine's current memory state. Your values can differ from every example.
- Display the live filesystem table by running this command:
df -h
Exercise 2: Find the Root Row
Find the row whose final field is /. That row describes the root filesystem.
Identify its size, used space, available space, and usage percentage. Every value is specific to your machine.
Parse the root filesystem
The root device name can vary between machines. The parser locates the row by checking for / in its final field.
- Switch back to VS Code from earlier.
- Find the block that starts with process_result = run_command(["pgrep", "-x", "no_such_process_x"]) near the bottom of script.py.
- Replace that block with this disk parser and its command call:
def parse_disk(output):
for line in output.splitlines()[1:]:
fields = line.split()
if len(fields) >= 6 and fields[-1] == "/":
return {
"size": fields[-5],
"used": fields[-4],
"available": fields[-3],
"use_percent": fields[-2],
}
return {"error": "Root filesystem row not found"}
disk_result = run_command(["df", "-h"])
print(parse_disk(disk_result["stdout"]))
What does the disk parser do?
- The slice in output.splitlines()[1:] skips the table header.
- The line variable holds one filesystem row during each loop.
- The fields variable holds that row as separate values.
- The condition selects the row whose final field is /.
- The returned dictionary stores the size, used space, available space, and usage percentage.
- Save script.py.
Before you run the script, what dictionary keys do you expect the root filesystem row to produce?
- Run the disk parser from VS Code's integrated terminal with this command:
python3 script.py
What should you see?
You will see a dictionary shaped like {'size': '<machine value>', 'used': '<machine value>', 'available': '<machine value>', 'use_percent': '<machine value>%'}. The values come from your root filesystem.
🙋♀️ Seeing a Root Row Error?
A common mistake is treating the header row as filesystem data. Keep [1:] after splitlines() so the loop begins with the first data row.
Help me check why my root filesystem row is not being parsed.
Parse the memory row
The memory table places its main memory values on the second line. A mebibyte is 1,048,576 bytes.
- Switch back to VS Code from earlier.
- Delete the two lines at the bottom of script.py that run and print the disk result.
- Paste this memory parser with the final command calls below parse_disk():
def parse_memory(output):
lines = output.splitlines()
if len(lines) >= 2:
fields = lines[1].split()
if len(fields) >= 7:
return {
"total_mib": fields[1],
"used_mib": fields[2],
"available_mib": fields[-1],
}
return {"error": "Memory row not found"}
disk_result = run_command(["df", "-h"])
memory_result = run_command(["free", "-m"])
print(parse_disk(disk_result["stdout"]))
print(parse_memory(memory_result["stdout"]))
What does the memory parser do?
- The lines list holds every row returned by the memory command.
- The parser selects lines[1] because that row contains the main memory values.
- The length checks protect the parser from incomplete output.
- The returned dictionary stores total, used, and available memory in mebibytes.
- Save script.py.
Before you run the final check, do you expect both parsers to return named values from the live tables?
- Verify both parsers by running this command:
python3 script.py
What should you see?
The first line is a disk dictionary shaped like {'size': '<machine value>', 'used': '<machine value>', 'available': '<machine value>', 'use_percent': '<machine value>%'}.
The second line is a memory dictionary shaped like {'total_mib': '<machine value>', 'used_mib': '<machine value>', 'available_mib': '<machine value>'}. Every numeric value reflects your machine.
🙋♀️ Seeing a Memory Row Error?
A common mistake is relying on fixed column spacing. Keep split() without a separator so runs of whitespace become clean fields.
Help me check why my memory row is not being parsed.
✔️ Awesome, I've got everything!
Your disk and memory parsers are working. Double-check that you saved script.py.
ⓧ I'd like to double check the full code
import subprocess
def run_command(command):
try:
result = subprocess.run(command, capture_output=True, text=True)
except OSError as error:
return {
"command": command,
"ok": False,
"returncode": None,
"stdout": "",
"stderr": str(error),
}
return {
"command": command,
"ok": result.returncode == 0,
"returncode": result.returncode,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
}
def parse_disk(output):
for line in output.splitlines()[1:]:
fields = line.split()
if len(fields) >= 6 and fields[-1] == "/":
return {
"size": fields[-5],
"used": fields[-4],
"available": fields[-3],
"use_percent": fields[-2],
}
return {"error": "Root filesystem row not found"}
def parse_memory(output):
lines = output.splitlines()
if len(lines) >= 2:
fields = lines[1].split()
if len(fields) >= 7:
return {
"total_mib": fields[1],
"used_mib": fields[2],
"available_mib": fields[-1],
}
return {"error": "Memory row not found"}
disk_result = run_command(["df", "-h"])
memory_result = run_command(["free", "-m"])
print(parse_disk(disk_result["stdout"]))
print(parse_memory(memory_result["stdout"]))
How should you use this reference?
Compare this file with your saved version from top to bottom. The two printed dictionaries confirm that both helpers receive captured command output.
Parsing changes command text into named values that are easier to test, print, and save.
Your live disk and memory output now has a reusable structure. Next, you will combine these values into the complete system snapshot.
Build the System Snapshot
Your disk parser now turns live command output into named values. Your memory parser does the same.
Separate command experiments help you learn each part. A repeatable DevOps report needs to collect everything in one run.
This step combines the parsed values with uptime. The finished script preserves the complete result as JSON.
In this step, get ready to:
- Collect disk space plus memory plus uptime results.
- Turn unavailable commands into friendly values.
- Save the complete snapshot as JSON.
Run the three system checks
Your existing run_command() function already records each command result. A new result_error() helper converts unsuccessful results into messages the snapshot can store.
- Return to script.py in VS Code.
- Find the import subprocess line at the top of the file.
- Replace that line with these imports:
import json
import subprocess
What do these imports provide?
- The json module converts the final snapshot dictionary into JSON.
- The existing subprocess module continues to run the Linux commands.
- Find the closing brace of the final dictionary returned by run_command().
- Add this helper immediately below run_command():
def result_error(result):
if result["stderr"]:
return result["stderr"]
if result["returncode"] is None:
return "Command could not start"
return f"Command returned code {result['returncode']}"
How does failure become readable?
- The helper returns captured error text when a command provides it.
- A missing return code means the command could not start.
- Any other unsuccessful result becomes a message containing its return code.
- Save script.py.
- Confirm the existing parser experiment still runs in VS Code's integrated terminal.
- Run the script with this command:
python3 script.py
What should you see?
You should still see the disk dictionary followed by the memory dictionary. The script should finish without a traceback.
🙋♀️ Seeing a traceback after adding the helper?
Check that result_error() starts at the left edge of the file. Make sure it sits below the complete run_command() function.
Ask for help with the function placement if the traceback continues: help me place result_error() correctly below run_command() in script.py
The final report needs a single inventory of commands. The commands dictionary keeps that inventory limited to the three required checks.
- Find disk_result = run_command(["df", "-h"]) near the bottom of script.py.
- Delete that line through the final print(parse_memory(memory_result["stdout"])) line.
- Add the new command inventory plus result handling in the cleared space:
commands = {
"disk": ["df", "-h"],
"memory": ["free", "-m"],
"uptime": ["uptime"],
}
results = {}
for name, command in commands.items():
results[name] = run_command(command)
if results["disk"]["ok"]:
disk = parse_disk(results["disk"]["stdout"])
else:
disk = {"error": result_error(results["disk"])}
if results["memory"]["ok"]:
memory = parse_memory(results["memory"]["stdout"])
else:
memory = {"error": result_error(results["memory"])}
if results["uptime"]["ok"]:
uptime = results["uptime"]["stdout"]
else:
uptime = f"unavailable ({result_error(results['uptime'])})"
print(f"Uptime: {uptime}")
How are the results assembled?
- The commands dictionary contains only the disk check plus memory check plus uptime check.
- The loop sends every argument list through the same command runner.
- Each condition parses a successful result or stores a friendly error value.
- The uptime value stays as cleaned text because its complete line is useful.
- Save script.py.
- Check the first combined result by running this command in VS Code's integrated terminal:
python3 script.py
What does this run prove?
You should see Uptime: followed by one machine-specific uptime line. The same run has also collected disk plus memory results.
🙋♀️ Is uptime unavailable?
Check that the uptime entry contains a one-item list. Confirm that its spelling matches the command available in your terminal.
Ask for help if the command works directly but fails inside the script: help me diagnose why the uptime command is unavailable through subprocess.run() in script.py
Save the snapshot as JSON
The snapshot dictionary brings the parsed values together with the cleaned command records. Writing it with UTF-8 keeps the JSON file portable.
- Find the completed uptime condition near the bottom of script.py.
- Insert this snapshot plus file-writing block immediately above print(f"Uptime: {uptime}"):
snapshot = {
"disk": disk,
"memory": memory,
"uptime": uptime,
"commands": results,
}
with open("system_snapshot.json", "w", encoding="utf-8") as json_file:
json.dump(snapshot, json_file, indent=2)
What gets saved?
- The parsed disk plus memory entries hold reusable system values.
- The uptime entry holds the cleaned live status line.
- The commands entry preserves stdout plus stderr plus return codes.
- The indentation makes system_snapshot.json easier to inspect in an editor.
- Save script.py.
- Create the JSON snapshot by running the script again:
python3 script.py
What does this run create?
The terminal should still show the machine-specific uptime line. The same run creates system_snapshot.json beside script.py.
- Return to VS Code.
- Open system_snapshot.json from the same folder as script.py.
You should see indented JSON with disk plus memory plus uptime plus commands keys. Their values reflect your current machine.
Cannot find the JSON file?
Confirm that Xfce Terminal is still using the folder containing script.py. Check the terminal output for a traceback from the file-writing block.
Ask for help if the script finishes but the file is missing: help me find why script.py is not creating system_snapshot.json in the same folder
Print and test the final summary
The JSON file is useful for later automation. A terminal summary gives you the important values immediately.
- Find this final line in script.py:
print(f"Uptime: {uptime}")
What does the current line show?
The current output exposes only the uptime value. The final display adds disk plus memory status around it.
- Replace that one line with the complete summary block:
print("SYSTEM SNAPSHOT")
if "error" in disk:
print(f"Disk: unavailable ({disk['error']})")
else:
print(f"Disk: {disk['used']} used of {disk['size']} ({disk['use_percent']})")
if "error" in memory:
print(f"Memory: unavailable ({memory['error']})")
else:
print(
f"Memory: {memory['used_mib']} MiB used of {memory['total_mib']} MiB; "
f"{memory['available_mib']} MiB available"
)
print(f"Uptime: {uptime}")
print("Saved: system_snapshot.json")
How does the summary stay friendly?
- Each error check prints an unavailable message when its command failed.
- Successful disk output includes used space plus total size plus percentage.
- Successful memory output includes used plus total plus available mebibytes.
- The final message confirms the JSON filename.
- Save script.py.
Before you run the final report, which fixed labels do you expect to see around the machine-specific values?
- Display the complete system snapshot by running:
python3 script.py
What should you see?
You should see SYSTEM SNAPSHOT followed by disk plus memory plus uptime lines. The final line should read Saved: system_snapshot.json.
Your sizes plus percentages plus uptime details are machine-specific. A successful run ends without a traceback.
A useful system report also survives a command that cannot start. This final test proves the snapshot keeps its other data during that failure.
- Find the "uptime": ["uptime"], entry in the commands dictionary.
- Temporarily replace the executable value uptime with no_such_command_x.
- Save script.py.
Before you run the failure test, do you think one unavailable command stops the JSON file from being saved?
- Test the failure path by running:
python3 script.py
This failure is intentional
You should see Uptime: unavailable plus a friendly explanation. You should still see Saved: system_snapshot.json.
The disk plus memory sections should still contain live values. The script should finish without a traceback.
- Replace no_such_command_x with uptime.
- Save script.py.
- Confirm that the restored entry matches this line:
"uptime": ["uptime"],
Why restore this entry?
The completed snapshot requires the real uptime command. Restoring the entry returns the project to its working end state.
Before the last run, what should replace the unavailable uptime message now that the required command is restored?
- Perform the end-of-step verification by running:
python3 script.py
Your snapshot is complete
You should see the full SYSTEM SNAPSHOT summary with live disk plus memory plus uptime values. The saved-file message confirms that system_snapshot.json was written again.
That closes the loop. Your reusable command runner now produces a readable terminal report plus a failure-safe JSON record.
✔️ Awesome, I've got everything!
Great work. Keep script.py plus system_snapshot.json in the same project folder.
ⓧ I'd like to double check the full code
import json
import subprocess
def run_command(command):
try:
result = subprocess.run(command, capture_output=True, text=True)
except OSError as error:
return {
"command": command,
"ok": False,
"returncode": None,
"stdout": "",
"stderr": str(error),
}
return {
"command": command,
"ok": result.returncode == 0,
"returncode": result.returncode,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
}
def result_error(result):
if result["stderr"]:
return result["stderr"]
if result["returncode"] is None:
return "Command could not start"
return f"Command returned code {result['returncode']}"
def parse_disk(output):
for line in output.splitlines()[1:]:
fields = line.split()
if len(fields) >= 6 and fields[-1] == "/":
return {
"size": fields[-5],
"used": fields[-4],
"available": fields[-3],
"use_percent": fields[-2],
}
return {"error": "Root filesystem row not found"}
def parse_memory(output):
lines = output.splitlines()
if len(lines) >= 2:
fields = lines[1].split()
if len(fields) >= 7:
return {
"total_mib": fields[1],
"used_mib": fields[2],
"available_mib": fields[-1],
}
return {"error": "Memory row not found"}
commands = {
"disk": ["df", "-h"],
"memory": ["free", "-m"],
"uptime": ["uptime"],
}
results = {}
for name, command in commands.items():
results[name] = run_command(command)
if results["disk"]["ok"]:
disk = parse_disk(results["disk"]["stdout"])
else:
disk = {"error": result_error(results["disk"])}
if results["memory"]["ok"]:
memory = parse_memory(results["memory"]["stdout"])
else:
memory = {"error": result_error(results["memory"])}
if results["uptime"]["ok"]:
uptime = results["uptime"]["stdout"]
else:
uptime = f"unavailable ({result_error(results['uptime'])})"
snapshot = {
"disk": disk,
"memory": memory,
"uptime": uptime,
"commands": results,
}
with open("system_snapshot.json", "w", encoding="utf-8") as json_file:
json.dump(snapshot, json_file, indent=2)
print("SYSTEM SNAPSHOT")
if "error" in disk:
print(f"Disk: unavailable ({disk['error']})")
else:
print(f"Disk: {disk['used']} used of {disk['size']} ({disk['use_percent']})")
if "error" in memory:
print(f"Memory: unavailable ({memory['error']})")
else:
print(
f"Memory: {memory['used_mib']} MiB used of {memory['total_mib']} MiB; "
f"{memory['available_mib']} MiB available"
)
print(f"Uptime: {uptime}")
print("Saved: system_snapshot.json")
How the full script fits together
- The command runner captures status plus output for every check.
- The parsers convert disk plus memory text into named fields.
- The snapshot dictionary combines parsed data with the original command records.
- The final section writes JSON before printing the friendly summary.
Secret mission
Add a Disk-Usage Warning
Turn the parsed root percentage into a clear operational signal. You will test both outcomes before restoring the project-defined 80 percent policy.
Clean Up Your Resources
Clean Up Your Resources
Choose a keep, pause, or delete path for your local project files. This local project has no ongoing costs.
Resources you used:
- The reusable system snapshot script at script.py.
- The generated system report at system_snapshot.json.
Keep everything running
No action is needed. Choose this option if you want to keep testing or extending your system snapshot.
- Keep script.py in the project folder from earlier.
- Keep system_snapshot.json beside the script for future comparisons.
- Retain the disk-warning extension for future system checks.
Pause - I'll come back to this later
The script ends after every run, so no background service needs to be stopped. Closing your tools clears the workspace while preserving both project files.
- Save any open changes in script.py.
- Close Xed.
- Close Xfce Terminal.
- Leave script.py in the project folder from earlier.
- Leave system_snapshot.json in the same folder.
Delete - I don't want to use this again
This option removes the script and its generated report. Your installed tools remain available for other projects.
Deletion Removes Both Project Files
Deleting these files can feel final because the command removes your completed work. A backup is your only recovery path after the deletion finishes.
- Confirm that Xfce Terminal is in the project folder from earlier by running this command:
ls
What Does This Check Do?
The ls command lists the files in the current folder. This check prevents you from deleting files in a different location.
You should see script.py and system_snapshot.json in the output.
- Delete both local project files by running this command:
rm script.py system_snapshot.json
What Does This Deletion Do?
This command removes script.py and system_snapshot.json from the current folder. It leaves Python and your Linux applications untouched.
- Verify the deletion by listing the folder contents again:
ls
What Does the Final Check Do?
The final file listing confirms the state of the project folder after deletion. It gives you visible proof that both project files are gone.
You should no longer see script.py or system_snapshot.json in the output.
Still See the Project Files?
- Check that the file names in the deletion command match script.py and system_snapshot.json exactly.
- Confirm that you ran the deletion from the same project folder that listed both files.
Help me remove these two local project files safely.
Nice Work!
Nice Work!
Brilliant work! Your Python system snapshot now turns live Linux data into a failure-safe JSON report.
You've learned how to:
- Build a reusable command runner around subprocess.run(). It captures stdout plus stderr as text. It also records each return code.
- Handle unsuccessful commands with return-code checks plus OSError handling. A failed command now produces a friendly status instead of stopping the script.
- Parse live disk plus memory output into named system values. The script saves those values in system_snapshot.json while printing a readable terminal summary.
- (Secret Mission) Add a disk-usage warning from the parsed root percentage. The check applies the project-defined 80 percent threshold without breaking the existing failure path.
Ready to quiz yourself?