Build a Safe Text Command Menu
Parse and execute add, remove, and show commands against an in-memory inventory.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Build a Safe Text Command Menu
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A command menu connects parsing to state changes. The user can type add "Blue notebook" 2, show, or remove "Blue notebook". A naive sequence of split and dictionary updates can corrupt inventory when a name contains spaces or a quantity is invalid. The menu's first input adds a two-word item. Treat the line as a tiny language rather than a free-form request. The verb is add, the name is Blue notebook, and the count is the text 2 until validation converts it. shlex handles the quoted boundary but does not decide whether these arguments satisfy the add grammar. The inventory is intentionally local and small.
- 00:41
There is no subprocess and no permanent storage, so every change is visible in one dictionary. That makes it possible to inspect whether a failed command changed state when it should not have. Before the first run, make a concrete prediction about the terminal output. A prediction turns a passive demonstration into a test: we can compare the actual result with the rule we thought the code followed. Keep the input small enough to inspect every value. Design a small grammar first: add requires a name and positive whole-number quantity; remove requires a known name; show takes no extra arguments. shlex.split handles quoting, while explicit branches decide the meaning of each command.
- 01:25
Unknown verbs and wrong argument counts produce ValueError with no mutation. Write down the exact grammar before editing the state: add needs two arguments, remove needs one, and show needs none. When a command has too many or too few tokens, stop with ValueError. This separates a parse error from the business rule that a quantity must be a positive integer. The naive starter both misreads the quoted name and mutates the caller's dictionary. A command menu can appear to work for one- word names while still failing the first realistic item. We need the grammar and the state policy together. Watch the output as evidence rather than jumping directly to a replacement snippet.
- 02:10
The broken program may exit cleanly while still violating its contract. We will name the exact input, the observed result, and the result we wanted before changing code. Keep state in a caller-owned dictionary and return a fresh dictionary after a successful change. Copying avoids partially changing the original if validation fails. Sort the keys for display so a run has stable output. The browser task uses no shell, files, or network calls. The second run illustrates why the input inventory is caller-owned. Copy the dictionary before a successful update so a later comparison can show that the original still has only the pen.
- 02:52
A rejected command must not mutate either the original or a returned state. This is especially important when parsing and conversion can fail. For add, convert the count and reject zero or negative values. For remove, check membership before deletion. These operations are small, but the order matters: validate the full command first, copy once, then apply the one intended transition. Avoid broad exception handling that hides a programming error behind an input message. Watch the input mapping before and after the copied update. The copy is a new result, not a hidden side effect.
- 03:33
If a conversion fails, no new mapping should be handed back and the original should remain exactly as it was. The intermediate run isolates one mechanism. It is not the entire solution, and that is intentional. A small diagnostic is easier to explain than a large corrected application because it shows which assumption failed and which Python behavior we can rely on. Implement run_menu(state, line). Use shlex.split. For add NAME COUNT, set that item's count to the positive integer in a copied dictionary, replacing its previous count if present; for remove NAME, delete a known name; for show, return a copied dictionary unchanged.
- 04:17
Reject unknown commands, wrong argument counts, malformed quotes, nonpositive counts, and missing remove targets with ValueError. Keep the input state unchanged. Parse first, check the verb and exact token count, validate quantity or target, then copy state and apply the change. Test add with a quoted two-word name. The final run treats 2 as valid and 0 and x as invalid for different reasons. Zero converts to an integer but violates the positive-count rule; x fails conversion. Those are both user-input errors, yet neither should change inventory. Test both to avoid accepting a value merely because int() succeeded.
- 04:59
The on-screen fix focuses on add to keep the code readable. The full project task extends the same parse–validate–copy sequence to remove and show. Unknown commands are errors; they are never handed to the operating system. After the fix, compare the observed output with the original prediction. The code shown on screen is executed locally and the terminal pane replays that captured run. The linked lesson gives you a separate checked task, so you can apply the idea rather than only copy a slide. The project intentionally stays small enough to reason about: no persistence, concurrency, or user accounts.
- 05:40
In this menu, add sets the item's count, replacing an earlier count for the same name. A production CLI would also need tests for Unicode names, error messages, and storage. Here the contract is to separate parsing, validation, and state transition and prove that an invalid command leaves the original inventory alone. A real command menu would format the resulting inventory and perhaps save it to disk. This browser project explicitly makes add replace the existing count for that name. It keeps state in memory and returns a new dictionary, making the core parse–validate–apply sequence easy to check.
- 06:21
The original mapping remains usable after a rejected command. Treat tokens as application data and keep the grammar explicit. The final run checks a removal and two invalid counts. Zero is an integer but violates the rule, while x is not an integer at all. Treat both as rejected input and confirm the original state stays unchanged. An edge case is more valuable than repeating the same happy path. The final run challenges a boundary or invalid input and explains the decision the function makes. If your own implementation disagrees, use the lesson console to narrow the smallest failing case.
Understand the concept
A command menu connects parsing to state changes. The user can type add "Blue notebook" 2, show, or remove "Blue notebook". A naive sequence of split and dictionary updates can corrupt inventory when a name contains spaces or a quantity is invalid.
Design a small grammar first: add requires a name and positive whole-number quantity; remove requires a known name; show takes no extra arguments. shlex.split handles quoting, while explicit branches decide the meaning of each command. Unknown verbs and wrong argument counts produce ValueError with no mutation.
Keep state in a caller-owned dictionary and return a fresh dictionary after a successful change. Copying avoids partially changing the original if validation fails. Sort the keys for display so a run has stable output. The browser task uses no shell, files, or network calls.
The project intentionally stays small enough to reason about: no persistence, concurrency, or user accounts. In this menu, add sets the item's count, replacing an earlier count for the same name. A production CLI would also need tests for Unicode names, error messages, and storage. Here the contract is to separate parsing, validation, and state transition and prove that an invalid command leaves the original inventory alone.
- Parse then validate
- Reject bad commands before changing state
- Return a fresh state mapping
- Sort output for deterministic checks
See it step by step
Read the code, predict the output, then compare it with the result.
01. Tokenize a command
import shlex
print(shlex.split('add "Blue notebook" 2'))['add', 'Blue notebook', '2']A quoted name survives tokenization.
02. Copy before updating
original={'pen':1}
updated=original.copy()
updated['book']=2
print(original)
print(updated){'pen': 1}
{'pen': 1, 'book': 2}The caller's original mapping has not changed.
03. Validate positive quantities
for value in ('2','0','x'):
try:
amount=int(value)
if amount <= 0: raise ValueError('positive count required')
print(amount)
except ValueError:
print('invalid')2
invalid
invalidConversion and range validation are separate steps.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Parse an inventory command with a quoted name
The menu's first input adds a two-word item. Treat the line as a tiny language rather than a free-form request. The verb is add, the name is Blue notebook, and the count is the text 2 until validation converts it. shlex handles the quoted boundary but does not decide whether these arguments satisfy the add grammar.
Write down the exact grammar before editing the state: add needs two arguments, remove needs one, and show needs none. When a command has too many or too few tokens, stop with ValueError. This separates a parse error from the business rule that a quantity must be a positive integer.
import shlex
print(shlex.split('add "Blue notebook" 2'))['add', 'Blue notebook', '2']- Tokenize without losing the quoted name.
- Branch on an allowlisted verb.
- Check the exact number of arguments.
Change a copy only after validation
The second run illustrates why the input inventory is caller-owned. Copy the dictionary before a successful update so a later comparison can show that the original still has only the pen. A rejected command must not mutate either the original or a returned state. This is especially important when parsing and conversion can fail.
For add, convert the count and reject zero or negative values. For remove, check membership before deletion. These operations are small, but the order matters: validate the full command first, copy once, then apply the one intended transition. Avoid broad exception handling that hides a programming error behind an input message.
original={'pen':1}
updated=original.copy()
updated['book']=2
print(original)
print(updated){'pen': 1}
{'pen': 1, 'book': 2}- Validate the quantity or removal target.
- Copy the mapping after validation.
- Return the new mapping and compare the original.
Try invalid counts before they reach state
The final run treats 2 as valid and 0 and x as invalid for different reasons. Zero converts to an integer but violates the positive-count rule; x fails conversion. Those are both user-input errors, yet neither should change inventory. Test both to avoid accepting a value merely because int() succeeded.
A real command menu would format the resulting inventory and perhaps save it to disk. This browser project explicitly makes add replace the existing count for that name. It keeps state in memory and returns a new dictionary, making the core parse–validate–apply sequence easy to check.
for value in ('2','0','x'):
try:
amount=int(value)
if amount <= 0: raise ValueError('positive count required')
print(amount)
except ValueError:
print('invalid')2
invalid
invalid- Test conversion separately from the range check.
- Verify invalid commands leave state untouched.
- Use the matching lesson's checked task to complete all three verbs.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Parse first, check the verb and exact token count, validate quantity or target, then copy state and apply the change. Test add with a quoted two-word name.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Changing state before validating
state[name] = int(count_text)Validate, then copy and updateWhat happens: Bad later input can leave state partly changed.
The original mapping remains usable after a rejected command.
Using shell-style text as a shell command
subprocess.run(line, shell=True)Dispatch only add, remove, show branchesWhat happens: The menu grants unintended system operations.
Treat tokens as application data and keep the grammar explicit.