| .. | ||
| README.md | ||
| stack.py | ||
| test_stack.py | ||
Stack
Original assessment brief: https://github.com/turner-townsend/backend-assessment/blob/master/README.md#stack
A Python implementation of the Fifth stack-based programming language, demonstrating clean code practices, robust error handling, and modern Python features.
What is Fifth?
Fifth is a stack-based language that operates on a stack of integers. All operations work with the top elements of the stack, following the last-in-first-out (LIFO) principle. This implementation provides an interactive REPL (read-eval-print loop) for executing Fifth commands.
Features
- Stack operations:
PUSH,POP,SWAP,DUP - Arithmetic operations:
+,-,*,/(floor division) - Comprehensive error handling: Input validation, stack state checking, and clear error messages
- Interactive REPL: Command-line interface with readline support for command history (↑/↓)
- Case-insensitive commands: Commands work in both upper and lowercase
Installation and usage
Requirements
- Python 3.9+ (uses modern type hint syntax
list[int]) - No external dependencies
Running the interpreter
python <filename>.py
The interpreter will start with a help message and enter interactive mode, displaying the current stack state before each command.
Commands reference
| Command | Description | Stack effect | Example |
|---|---|---|---|
PUSH <n> |
Push integer n onto stack | [] → [n] |
PUSH 42 |
POP |
Remove top element | [..., n] → [...] |
POP |
SWAP |
Swap top two elements | [..., a, b] → [..., b, a] |
SWAP |
DUP |
Duplicate top element | [..., n] → [..., n, n] |
DUP |
+ |
Add top two elements | [..., a, b] → [..., (a+b)] |
+ |
- |
Subtract (second - top) | [..., a, b] → [..., (a-b)] |
- |
* |
Multiply top two elements | [..., a, b] → [..., (a*b)] |
* |
/ |
Floor divide (second / top) | [..., a, b] → [..., (a//b)] |
/ |
HELP |
Show available commands | No change | HELP |
EXIT |
Quit the program | - | EXIT |
Example session
Available commands: HELP, PUSH, POP, SWAP, DUP
Available operations: +, -, *, /
stack is []
PUSH 3
stack is [3]
PUSH 11
stack is [3, 11]
+
stack is [14]
DUP
stack is [14, 14]
PUSH 2
stack is [14, 14, 2]
*
stack is [14, 28]
SWAP
stack is [28, 14]
/
stack is [2]
+
ERROR: two numbers required
POP
stack is []
exit
Tests
Run the test suite:
pytest -v
Example output:
➜ stack git:(main) ✗ pytest -v
================================================================ test session starts ================================================================
platform linux -- Python 3.12.2, pytest-8.3.4, pluggy-1.5.0 -- /home/jamey/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/jamey/Projects/turner-townsend-backend-assessment/stack
plugins: mock-3.14.0, asyncio-0.21.2
asyncio: mode=Mode.STRICT
collected 22 items
test_stack.py::test_push_and_pop PASSED [ 4%]
test_stack.py::test_push_invalid_value PASSED [ 9%]
test_stack.py::test_pop_empty_stack PASSED [ 13%]
test_stack.py::test_swap PASSED [ 18%]
test_stack.py::test_swap_insufficient_elements PASSED [ 22%]
test_stack.py::test_dup PASSED [ 27%]
test_stack.py::test_dup_empty_stack PASSED [ 31%]
test_stack.py::test_addition PASSED [ 36%]
test_stack.py::test_subtraction PASSED [ 40%]
test_stack.py::test_multiplication PASSED [ 45%]
test_stack.py::test_division PASSED [ 50%]
test_stack.py::test_division_by_zero PASSED [ 54%]
test_stack.py::test_binary_op_insufficient_elements PASSED [ 59%]
test_stack.py::test_execute_unknown_command PASSED [ 63%]
test_stack.py::test_execute_push_command PASSED [ 68%]
test_stack.py::test_execute_pop_command PASSED [ 72%]
test_stack.py::test_execute_swap_command PASSED [ 77%]
test_stack.py::test_execute_dup_command PASSED [ 81%]
test_stack.py::test_help PASSED [ 86%]
test_stack.py::test_execute_empty_command PASSED [ 90%]
test_stack.py::test_execute_whitespace_command PASSED [ 95%]
test_stack.py::test_case_insensitivity PASSED [100%]
================================================================ 22 passed in 0.03s =================================================================
Design decisions and architecture
Core design philosophy
The implementation prioritises clarity and maintainability over performance optimisation, following the principle of being "explicit over implicit" and "discoverable over performant" as requested in the brief.
Key architectural choices
1. Class-based structure (FifthStack)
- Reasoning: Encapsulates state (the stack) with behaviour (operations), making the code more organised and testable
- Alternative considered: Functional approach with global state, rejected for testing difficulties and state management complexity
2. Command dictionary pattern
self.commands: dict[str, Callable] = {
"help": self.help,
"push": self.push,
# ...
}
- Reasoning: Makes adding new commands trivial and eliminates long if/elif chains
- Benefit: Extensible design - new commands require only adding to the dictionary
- Trade-off: Slight memory overhead for the dictionary, but gains significant maintainability
3. Separate binary operations dictionary
self.binary_ops: dict[str, Callable[[int, int], int]] = {
"+": operator.add,
# ...
}
- Reasoning: Leverages Python's
operatormodule for built-in mathematical operations - Benefit: Consistent behaviour with Python's arithmetic, less error-prone than custom implementations
- Design: Separate from commands because they have different signatures and error handling needs
4. Method extraction for each operation
Each stack operation is implemented as a separate method rather than inline logic:
- Testing: Each operation can be unit tested independently
- Debugging: Stack traces clearly identify which operation failed
- Extensibility: New operations are self-contained and don't affect existing code
- Readability: Each method has a single, clear responsibility
5. Defensive programming with _require() helper
def _require(self, number, message):
if len(self.stack) < number:
print(f"ERROR: {message}")
return False
return True
- Reasoning: Centralizes stack size validation logic
- Benefit: Consistent error messages and reduces code duplication
- DRY principle: Single source of truth for stack requirement checking
6. Error recovery strategy
- Division by zero: Restores original stack state (
self.stack.extend([a, b])) - Invalid input: Provides clear error messages without crashing
- Insufficient stack: Fails gracefully with descriptive messages
- Reasoning: Interpreter should be robust and not crash on user errors
7. Modern Python features used
Type hints with generic collections (Python 3.9+):
self.stack: list[int] = []
- Reasoning: Improves code readability and enables better IDE support
- Choice: Used
list[int]instead oftyping.List[int]for cleaner, modern syntax
Input processing centralization:
def _normalize_input(self, command: str) -> list[str]:
return command.lower().strip().split()
- Reasoning: Single source of truth for command normalization and tokenization
- Benefit: Both REPL and direct execution paths use identical processing logic
- Design choice: Avoided walrus operator in favour of explicit method extraction for better testability and clarity
f-string formatting:
- Reasoning: More readable than
.format()or%formatting - Performance: Faster than alternative string formatting methods
8. REPL design choices
Stack display: Shows stack state after each command
- Reasoning: Follows the specified output format from the brief
- Implementation choice: Used Python's native list representation for consistency with the expected format
- Benefit: Maintains familiar syntax for Python developers while meeting requirements
Case insensitive commands:
command.lower().strip().split()
- Reasoning: More user-friendly, follows principle of least surprise
- Benefit: Reduces user errors from capitalisation mistakes
Graceful exit: Handles both exit command and Ctrl+C/EOF
- Reasoning: Proper CLI applications should handle interrupts gracefully
- Implementation: Try/catch for
EOFErrorandKeyboardInterrupt
Alternative approaches considered
-
Parser-based approach: Could have used a formal parser (e.g. PLY), but rejected as over-engineering for this simple language
-
Functional approach: Could have used pure functions with immutable data but rejected because:
- Stack operations naturally mutate state
- Would require returning new stack state from every function
- Less intuitive for this particular problem domain
-
Command pattern with classes: Could have made each command a separate class but rejected due to:
- Unnecessary complexity for simple operations
- Would require more boilerplate code
- Current approach is more discoverable
-
eval() for arithmetic: Could have used
eval()for mathematical expressions but rejected due to:- Security concerns
- Need for custom stack-based evaluation order
- Loss of control over error handling
Testing strategy
The design facilitates testing through:
- Isolated methods: Each operation can be tested independently
- Dependency injection: Stack state can be set up for specific test scenarios
- Clear return values: Methods have predictable behaviour and side effects
- Error conditions: All error paths are reachable and testable
Extensibility
Adding new features is straightforward:
- New commands: Add to
self.commandsdictionary and implement method - New operators: Add to
self.binary_opsdictionary - New data types: Modify type hints and validation logic
- New output formats: Modify display logic in main loop
This architecture balances simplicity with software engineering practices, to keep the code readable and maintainable.