PracHub
QuestionsCoachesLearningGuidesInterview Prep

Quick Overview

This question evaluates parsing and string-processing skills, hierarchical data structure design for call-tree reconstruction, exception trace analysis, and algorithmic complexity reasoning for robust log handling.

  • medium
  • Anthropic
  • Coding & Algorithms
  • Software Engineer

Parse and Reconstruct Stack Trace

Company: Anthropic

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

Given a multi-line stack trace string from a single thread (e.g., each frame is in the form 'at Module::Function(file:line)'), design and implement a parser that: ( 1) extracts the ordered list of frames from most recent to oldest; ( 2) reconstructs the call hierarchy as a tree; and ( 3) identifies the deepest frame associated with the thrown exception. The parser must correctly handle nested causes (e.g., 'Caused by:' sections), abbreviated repeated frames (e.g., '... N more'), and missing or malformed lines. Describe your data structures, time/space complexity, and provide working code for parse and reconstruction.

Quick Answer: This question evaluates parsing and string-processing skills, hierarchical data structure design for call-tree reconstruction, exception trace analysis, and algorithmic complexity reasoning for robust log handling.

Part 1: Extract Frames from the Outermost Stack Trace Section

Extract the valid stack-frame lines from the **outermost (first) exception section** of a multi-line stack trace. ## Function ```python def solution(trace): ... ``` - **Input:** `trace` — a single string containing the whole stack trace, with lines separated by newlines. It may be empty. - **Output:** a **list of strings**, one per valid frame, in the same order they appear in the input (**most recent call first, oldest call last**). ## Line format Read the trace line by line. Before testing a line, **trim leading/trailing whitespace**. A line is a **valid frame** if, after trimming, it has the form: ``` at Module::Function(file:line) ``` Concretely, a trimmed line qualifies only when **all** of the following hold: - It starts with the prefix `at ` (the word `at` followed by a space). - Let **body** be the text after that prefix, with its own surrounding whitespace trimmed (e.g. `Module::Function(file:line)`). - **body** ends with a closing parenthesis `)`. - **body** contains an opening parenthesis `(` and, after it, a colon `:` (use the **last** `(` and the **last** `:`); the `(` must come before the `:`, and the `:` must not be the final character (so a line number follows it). - The text between that `:` and the closing `)` is the **line number**: it must be **non-empty and consist only of digits** (a non-negative integer). - The text before the `(` (the `Module::Function` part) must be **non-empty**. ## What to return for each frame For a valid frame, append the **body** — that is, the frame **without** the leading `at ` prefix and with surrounding whitespace removed (e.g. `App::run(app.py:10)`). Do not include `at ` in the output. ## Section boundary Scan the lines top to bottom. As soon as you reach a line whose **trimmed** form starts with `Caused by:`, **stop immediately** — that line and everything after it belongs to a nested cause and must be ignored. Only the first (outermost) exception section is parsed. ## Other rules - **Skip** any line that is blank, malformed, or does not match the valid-frame format (e.g. header lines like `RuntimeError: boom` or `random text`, or a frame whose body has no `(...:...)` shape such as `at BadLine(nope)`). - If `trace` is empty, or if a `Caused by:` line appears before any valid frame, return an **empty list**. - Preserve the original order of the valid frames you keep (most recent to oldest). ## Constraints - `0 <= len(trace) <= 200000` - The trace contains at most `10000` lines. - A valid frame line must start with `at ` after trimming and must end with `(file:line)`, where `line` is a non-negative integer. - Only the first exception section is parsed; later `Caused by:` sections are ignored. ## Examples **Input** ``` RuntimeError: boom at App::run(app.py:10) at Main::main(main.py:20) ``` **Output:** `['App::run(app.py:10)', 'Main::main(main.py:20)']` --- **Input** ``` ValueError random text at Parser::parse(parser.py:7) at BadLine(nope) Caused by: IOError at IO::read(io.py:3) ``` **Output:** `['Parser::parse(parser.py:7)']` (`ValueError` and `random text` are not frames; `at BadLine(nope)` has no `:` after its `(`, so it fails the `(file:line)` format; parsing stops at `Caused by:`, so `at IO::read(io.py:3)` is ignored.) --- **Input** ``` TopError Caused by: Inner at X::f(x.py:1) ``` **Output:** `[]` (the `Caused by:` line ends the section before any frame is collected.)

Constraints

  • 0 <= len(trace) <= 200000
  • The trace contains at most 10000 lines.
  • A valid frame line must start with 'at ' after trimming and must end with '(file:line)' where line is a non-negative integer.
  • Only the first exception section should be parsed; later 'Caused by:' sections are ignored.

Examples

Input: 'RuntimeError: boom\nat App::run(app.py:10)\nat Main::main(main.py:20)'

Expected Output: ['App::run(app.py:10)', 'Main::main(main.py:20)']

Explanation: Both frame lines are valid and belong to the outermost section.

Input: 'ValueError\nrandom text\n at Parser::parse(parser.py:7)\nat BadLine(nope)\nCaused by: IOError\nat IO::read(io.py:3)'

Expected Output: ['Parser::parse(parser.py:7)']

Explanation: The malformed line is skipped, and parsing stops before the cause section.

Input: ''

Expected Output: []

Explanation: Empty input has no frames.

Input: 'TopError\nCaused by: Inner\nat X::f(x.py:1)'

Expected Output: []

Explanation: There are no outermost frames before the first cause section.

Input: 'Oops\n at One::only(one.py:1) '

Expected Output: ['One::only(one.py:1)']

Explanation: Leading and trailing spaces should be ignored when validating a frame line.

Hints

  1. Process the trace line by line and stop at the first 'Caused by:' line.
  2. You do not need a full parser; checking the position of '(', ':', and ')' is enough to validate a frame.

Part 2: Reconstruct a Merged Call Hierarchy Tree

Reconstruct the merged call hierarchy of a single thread's stack trace and return it as a **forest of nested dictionaries**. ## Function ```python def solution(trace): ... ``` `trace` is a single string containing the full multi-line stack trace (lines separated by `\n`). Return a nested dictionary of the form `{frame: child_subtree}` (see **Output** below). ## Background A stack trace may contain several **exception sections** chained by `Caused by:` lines, and a section may abbreviate its repeated tail using a `... N more` line. Your job is to rebuild every section's full call stack and merge them all into one tree. ## Frame lines After trimming surrounding whitespace, a **valid frame line** has the form: ``` at Module::Function(file:line) ``` A line is a valid frame **only if** the trimmed line: - starts with `at ` (the literal `at` followed by a space), and - ends with `)`, and - contains an opening `(` that comes before the final `:`, and - has a non-empty part before that `(`, and - has a **line number** (the text between the final `:` and the closing `)`) consisting only of digits (a non-negative integer). The **frame** you record is the part of the line **after** `at ` (for example, the line `at B::b(b.py:2)` yields the frame `B::b(b.py:2)`). Lines that do not satisfy every rule above are **malformed** and must be ignored. ## Sections - The **first section** begins at the start of the trace. - Each line whose trimmed form starts with `Caused by:` ends the current section and begins a **new section** (the `Caused by:` line itself is not a frame). - Within a section, valid frame lines are collected **in the order they appear**, which is most recent call first down to oldest call. ## The `... N more` abbreviation A line whose trimmed form starts with `... ` and ends with ` more`, with a digit-only number `N` in between, is an **abbreviation line**. Each section contains **at most one** such line. When a section (other than the first) has an abbreviation `... N more`, append the **last N frames of the previous section's fully reconstructed stack** to this section's collected frames: - "Previous" means the section immediately before this one, **after** that section has itself been fully reconstructed (so abbreviations chain transitively through `Caused by:` sections). - If `N` is larger than the number of frames in the previous reconstructed stack, append **all** of them. - An abbreviation line in the first section (which has no previous section) contributes nothing. ## Output Insert every fully reconstructed stack into a single tree, walking each stack from its **oldest call to its most recent call**. The tree is represented by nested dictionaries: each frame maps to the dictionary of its child frames, e.g. `{frame: {childFrame: {...}}}`. A leaf frame maps to an empty dictionary `{}`. - Stacks that share a common prefix of oldest calls share those nodes in the tree. - If multiple disconnected roots exist, they all appear at the **top level** of the returned dictionary. - An empty reconstructed stack (for example, a section with only malformed lines) contributes nothing. ## Edge cases - An empty `trace` returns `{}`. - Malformed lines, blank lines, and exception-description lines are silently skipped. ## Example Input: ``` Outer at Service::run(s.py:30) at Controller::handle(c.py:20) at Main::main(m.py:10) Caused by: ParseError at Parser::parse(p.py:5) ... 2 more ``` The first section's stack (most recent → oldest) is `Service::run`, `Controller::handle`, `Main::main`. The second section starts with `Parser::parse`, then `... 2 more` appends the last 2 frames of the previous reconstructed stack (`Controller::handle`, `Main::main`), giving `Parser::parse`, `Controller::handle`, `Main::main`. Inserting both stacks oldest-first and merging the shared `Main::main → Controller::handle` prefix produces: ```python {"Main::main(m.py:10)": {"Controller::handle(c.py:20)": {"Service::run(s.py:30)": {}, "Parser::parse(p.py:5)": {}}}} ``` ## Constraints - `0 <= len(trace) <= 200000` - The trace contains at most 10000 lines. - Each section contains at most one abbreviation line of the form `... N more`. - A valid frame line must start with `at ` after trimming and must end with `(file:line)` where `line` is a non-negative integer.

Constraints

  • 0 <= len(trace) <= 200000
  • The trace contains at most 10000 lines.
  • Each section contains at most one abbreviation line of the form '... N more'.
  • A valid frame line must start with 'at ' after trimming and must end with '(file:line)' where line is a non-negative integer.

Examples

Input: 'Err\nat B::b(b.py:2)\nat A::a(a.py:1)'

Expected Output: {'A::a(a.py:1)': {'B::b(b.py:2)': {}}}

Explanation: A single stack becomes a single root-to-leaf path from oldest to newest.

Input: 'Outer\nat Service::run(s.py:30)\nat Controller::handle(c.py:20)\nat Main::main(m.py:10)\nCaused by: ParseError\nat Parser::parse(p.py:5)\n... 2 more'

Expected Output: {'Main::main(m.py:10)': {'Controller::handle(c.py:20)': {'Service::run(s.py:30)': {}, 'Parser::parse(p.py:5)': {}}}}

Explanation: The cause expands to [Parser::parse, Controller::handle, Main::main], creating a branch under Controller::handle.

Input: 'Top\nat A1::f(a1.py:4)\nat Root::main(root.py:1)\nCaused by: Mid\nat B1::g(b1.py:3)\n... 1 more\nCaused by: Low\nat C1::h(c1.py:2)\n... 2 more'

Expected Output: {'Root::main(root.py:1)': {'A1::f(a1.py:4)': {}, 'B1::g(b1.py:3)': {'C1::h(c1.py:2)': {}}}}

Explanation: Each deeper cause is expanded from the previous section, producing a shared prefix and a longer branch.

Input: ''

Expected Output: {}

Explanation: An empty trace has no hierarchy.

Input: 'E\nnoise\nat X::x(x.py:2)\nat Main::main(main.py:1)\nCaused by: Y\nbad\n... 5 more'

Expected Output: {'Main::main(main.py:1)': {'X::x(x.py:2)': {}}}

Explanation: The malformed line is ignored, and the oversized abbreviation copies only the available parent suffix.

Hints

  1. First reconstruct each section's full frame list; only then build the hierarchy.
  2. A trie built from oldest to most recent frames naturally merges shared call prefixes.

Part 3: Find the Deepest Frame of the Thrown Exception

# Find the Deepest Frame of the Thrown Exception Given a Java-style stack trace as a single string `trace`, return the **deepest frame of the actually-thrown exception**, or `None` if no such frame can be recovered. Implement: ```python def solution(trace): ... ``` ## Background A stack trace is divided into **sections**. The first section is the original error; each subsequent section is introduced by a line whose trimmed text begins with `Caused by:`. These sections form a *cause chain*, and the **last section in the chain is the actually-thrown exception** — the one whose deepest frame you must report. Within a section, frames are listed **most-recent first** (top) to **oldest last** (bottom). The call hierarchy is rooted at the oldest call, so the **deepest frame is the most-recent frame** — i.e. the **first** valid frame listed in the last section after expansion. ## Line types Parse the trace **line by line** (split on newlines). Each line, after trimming surrounding whitespace, is one of: - **Section header** — starts with `Caused by:`. Closes the current section and begins a new one. - **Frame line** — a valid frame has the trimmed form `at Module::Function(file:line)`. Specifically, after trimming it must: - start with `at ` (then take the remaining text as the **frame body**, e.g. `Module::Function(file:line)`), - end with `)`, - contain a `(` such that the text before the last `(` is **non-empty**, and - have, between the last `(` and the closing `)`, a `file:line` part whose portion **after the last `:` is a non-negative integer** (all digits). The value you ultimately return is this **frame body** (the text after `at `), e.g. `B::work(b.py:2)`. - **Abbreviation line** — the form `... N more` (trimmed text starts with `... ` and ends with ` more`, with `N` a non-negative integer in between). It means: this section's stack continues with the **last `N` frames of the previous section's fully reconstructed stack**. Each section contains **at most one** abbreviation line. - **Anything else** (including the leading exception-message line of a section, and any malformed frame) is **ignored**. ## Reconstruction Process sections in order. For each section, its reconstructed frame list is its own explicit valid frames followed by, **if it has a `... N more` and a previous section exists**, the last `k = min(N, len(previous_reconstructed))` frames of the **previous *fully reconstructed*** section (so inheritance composes across multiple nested causes). Inherited frames are appended **after** the section's own frames. ## Return value Return the **first frame** (index `0`, the most-recent / deepest) of the **last** section's reconstructed frame list. Return `None` when: - the trace is empty, or - the last section has **no** recoverable frame (no valid explicit frame and nothing recovered via `... N more`). Note: if the deepest section has no explicit valid frames, you may still recover its frames purely from `... N more`. ## Constraints - `0 <= len(trace) <= 200000` - The trace contains at most `10000` lines. - Each section contains at most one abbreviation line of the form `... N more`. - A valid frame line must start with `at ` after trimming and must end with `(file:line)` where `line` is a non-negative integer. ## Examples **Example 1** ``` Error at B::work(b.py:2) at A::main(a.py:1) ``` Single section; first valid frame is the deepest. Returns `B::work(b.py:2)`. **Example 2** ``` Outer at Service::run(s.py:3) at Main::main(m.py:1) Caused by: Inner at Parser::parse(p.py:2) ... 1 more ``` The last (thrown) section's first frame is `Parser::parse(p.py:2)`. Returns `Parser::parse(p.py:2)`. **Example 3** ``` Outer at X::x(x.py:2) at Main::main(m.py:1) Caused by: Inner bad line ... 2 more ``` The last section has no valid explicit frame; its frames are recovered from `... 2 more` (the last 2 frames of the reconstructed parent). The deepest is `X::x(x.py:2)`. Returns `X::x(x.py:2)`. **Example 4** ``` (empty string) ``` Returns `None`.

Constraints

  • 0 <= len(trace) <= 200000
  • The trace contains at most 10000 lines.
  • Each section contains at most one abbreviation line of the form '... N more'.
  • A valid frame line must start with 'at ' after trimming and must end with '(file:line)' where line is a non-negative integer.

Examples

Input: 'Error\nat B::work(b.py:2)\nat A::main(a.py:1)'

Expected Output: 'B::work(b.py:2)'

Explanation: With no nested cause, the thrown exception is the outermost section, and its deepest node is the most recent frame.

Input: 'Outer\nat Service::run(s.py:3)\nat Main::main(m.py:1)\nCaused by: Inner\nat Parser::parse(p.py:2)\n... 1 more'

Expected Output: 'Parser::parse(p.py:2)'

Explanation: The deepest cause expands to [Parser::parse, Main::main], so the deepest node is Parser::parse.

Input: 'Top\nat A::a(a.py:4)\nat Root::main(root.py:1)\nCaused by: Mid\nat B::b(b.py:3)\n... 1 more\nCaused by: Low\nat C::c(c.py:2)\n... 2 more'

Expected Output: 'C::c(c.py:2)'

Explanation: The final cause section reconstructs to [C::c, B::b, Root::main], and the deepest node is C::c.

Input: ''

Expected Output: None

Explanation: Empty input cannot produce a frame.

Input: 'Outer\nat X::x(x.py:2)\nat Main::main(m.py:1)\nCaused by: Inner\nbad line\n... 2 more'

Expected Output: 'X::x(x.py:2)'

Explanation: The deepest cause has no valid explicit frame, but expansion recovers [X::x, Main::main].

Hints

  1. The thrown exception is the last section, not the first one.
  2. To expand '... N more', reuse the suffix of the previous section's reconstructed frame list.
Last updated: Apr 26, 2026

Loading coding console...

PracHub

Master your tech interviews with 8,500+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • AI Coding Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • In-Memory Key-Value Database with Nested Transactions - Anthropic (medium)
  • Maximum-Length Unique-Character Subset - Anthropic (medium)
  • Banking System Simulation - Anthropic (medium)
  • LRU Cache - Anthropic (medium)
  • Account Balance with Expiring Grants - Anthropic (medium)