Skip to content

Add stubs for profiling#15652

Draft
johnslavik wants to merge 9 commits intopython:mainfrom
johnslavik:profiling-package
Draft

Add stubs for profiling#15652
johnslavik wants to merge 9 commits intopython:mainfrom
johnslavik:profiling-package

Conversation

@johnslavik
Copy link
Copy Markdown
Member

@johnslavik johnslavik commented Apr 20, 2026

Beta freeze is about to come, so here's a work-in-progress on profiling.

From here, I'd like to remove all private interfaces that aren't meant for public consumption and thus don't need to be in the typeshed.

johnslavik and others added 3 commits April 20, 2026 04:52
- Complete type annotations for profiling.tracing and profiling.sampling
- Replace all Incomplete usages with proper types throughout
- Add return types, parameter types, and attribute types to all stubs
- Only exception: unwinder: Incomplete in sample.pyi (_remote_debugging C ext)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@johnslavik
Copy link
Copy Markdown
Member Author

johnslavik commented Apr 20, 2026

Going through a monkey patched version of dead to find potentially unnecessary stuff:

Details
uv run dead --files 'typeshed/stdlib/profiling|cpython/Lib/test/test_profiling' --tests test_profiling | rg never
dump_stats is never read, defined in typeshed/stdlib/profiling/tracing/__init__.pyi:21
snapshot_stats is never read, defined in typeshed/stdlib/profiling/tracing/__init__.pyi:23
runcall is never read, defined in typeshed/stdlib/profiling/tracing/__init__.pyi:26
_Utils is never read, defined in typeshed/stdlib/profiling/tracing/_utils.pyi:4
MIN_SAMPLES_FOR_TUI is never read, defined in typeshed/stdlib/profiling/sampling/sample.pyi:20
MACOS_PERMISSION_ERROR is never read, defined in typeshed/stdlib/profiling/sampling/__main__.pyi:8
LINUX_PERMISSION_ERROR is never read, defined in typeshed/stdlib/profiling/sampling/__main__.pyi:9
WINDOWS_PERMISSION_ERROR is never read, defined in typeshed/stdlib/profiling/sampling/__main__.pyi:10
GENERIC_PERMISSION_ERROR is never read, defined in typeshed/stdlib/profiling/sampling/__main__.pyi:11
handle_permission_error is never read, defined in typeshed/stdlib/profiling/sampling/__main__.pyi:13
base_opcode is never read, defined in typeshed/stdlib/profiling/sampling/opcode_utils.pyi:1
MILLISECONDS_PER_SECOND is never read, defined in typeshed/stdlib/profiling/sampling/constants.pyi:2
_INTERNAL_FRAME_SUFFIXES is never read, defined in typeshed/stdlib/profiling/sampling/constants.pyi:15
_TemplateLoader is never read, defined in typeshed/stdlib/profiling/sampling/heatmap_collector.pyi:34
_TreeBuilder is never read, defined in typeshed/stdlib/profiling/sampling/heatmap_collector.pyi:44
build_file_tree is never read, defined in typeshed/stdlib/profiling/sampling/heatmap_collector.pyi:46
_HtmlRenderer is never read, defined in typeshed/stdlib/profiling/sampling/heatmap_collector.pyi:48
render_hierarchical_html is never read, defined in typeshed/stdlib/profiling/sampling/heatmap_collector.pyi:52
GECKO_CATEGORIES is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:6
CATEGORY_OTHER is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:7
CATEGORY_PYTHON is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:8
CATEGORY_NATIVE is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:9
CATEGORY_GC is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:10
CATEGORY_GIL is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:11
CATEGORY_CPU is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:12
CATEGORY_CODE_TYPE is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:13
CATEGORY_OPCODES is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:14
CATEGORY_EXCEPTION is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:15
DEFAULT_SUBCATEGORY is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:16
GECKO_FORMAT_VERSION is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:17
GECKO_PREPROCESSED_VERSION is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:18
RESOURCE_TYPE_LIBRARY is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:19
FRAME_ADDRESS_NONE is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:20
FRAME_INLINE_DEPTH_ROOT is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:21
PROCESS_TYPE_MAIN is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:22
STACKWALK_DISABLED is never read, defined in typeshed/stdlib/profiling/sampling/gecko_collector.pyi:23
get_stats is never read, defined in typeshed/stdlib/profiling/sampling/binary_collector.pyi:23, typeshed/stdlib/profiling/sampling/binary_reader.pyi:23
convert_binary_to_format is never read, defined in typeshed/stdlib/profiling/sampling/binary_reader.pyi:25
ArgumentError is never read, defined in typeshed/stdlib/profiling/sampling/_sync_coordinator.pyi:4
SyncError is never read, defined in typeshed/stdlib/profiling/sampling/_sync_coordinator.pyi:5
TargetError is never read, defined in typeshed/stdlib/profiling/sampling/_sync_coordinator.pyi:6
COMPRESSION_NONE is never read, defined in typeshed/stdlib/profiling/sampling/binary_collector.pyi:8
COMPRESSION_ZSTD is never read, defined in typeshed/stdlib/profiling/sampling/binary_collector.pyi:9
collect_failed_sample is never read, defined in typeshed/stdlib/profiling/sampling/binary_collector.pyi:19, typeshed/stdlib/profiling/sampling/collector.pyi:22, typeshed/stdlib/profiling/sampling/live_collector/collector.pyi:126
intern is never read, defined in typeshed/stdlib/profiling/sampling/string_table.pyi:3
get_string is never read, defined in typeshed/stdlib/profiling/sampling/string_table.pyi:4
get_strings is never read, defined in typeshed/stdlib/profiling/sampling/string_table.pyi:5
CustomFormatter is never read, defined in typeshed/stdlib/profiling/sampling/cli.pyi:38
DiffFlamegraphAction is never read, defined in typeshed/stdlib/profiling/sampling/cli.pyi:40
FORMAT_EXTENSIONS is never read, defined in typeshed/stdlib/profiling/sampling/cli.pyi:49
COLLECTOR_MAP is never read, defined in typeshed/stdlib/profiling/sampling/cli.pyi:50
get_dimensions is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:32, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:56, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:7
refresh is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:11, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:34, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:58
redraw is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:13, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:35, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:59
get_input is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:17, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:37, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:61
set_nodelay is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:19, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:38, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:62
has_colors is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:21, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:39, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:63
init_color_pair is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:23, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:40, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:64
get_color_pair is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:25, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:41, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:65
get_attr is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:27, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:42, typeshed/stdlib/profiling/sampling/live_collector/display.pyi:66
find_text is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/display.pyi:70
set_enabled is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/trend_tracker.pyi:12
get_trend is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/trend_tracker.pyi:14
format_rate_with_units is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/widgets.pyi:57
draw_thread_status is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/widgets.pyi:60
draw_finished_banner is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/widgets.pyi:63
draw_stats_rows is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/widgets.pyi:70
render_filter_input_prompt is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/widgets.pyi:76
increment_status_flag is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/collector.pyi:62
as_status_dict is never read, defined in typeshed/stdlib/profiling/sampling/live_collector/collector.pyi:63

Idea is: if it's not referenced in tests, it's a signal that you probably won't need this (there is some false negatives so I'll do this by hand).

johnslavik and others added 3 commits April 20, 2026 06:11
Removed:

- tracing/__main__.pyi: deleted — __main__ stubs are executable entry
  points, not importable API surface
- tracing/_utils.pyi: deleted — _Utils is a private helper used only
  inside the tracing module itself
- sampling/__main__.pyi: deleted — PERMISSION_ERROR constants and
  handle_permission_error are CLI implementation details, not importable
  API
- sampling/binary_collector.pyi: removed COMPRESSION_NONE, COMPRESSION_ZSTD
  — internal constants not part of user-facing API
- sampling/cli.pyi: removed CustomFormatter, DiffFlamegraphAction,
  FORMAT_EXTENSIONS, COLLECTOR_MAP, and associated imports — internal CLI
  scaffolding and dispatch dicts used only inside cli.py; also removed
  re-exports of errors/collectors/sample that duplicate their own modules
- sampling/constants.pyi: removed MICROSECONDS_PER_SECOND (re-exported
  only internally), MILLISECONDS_PER_SECOND and _INTERNAL_FRAME_SUFFIXES
  (private implementation details)
- sampling/gecko_collector.pyi: removed GECKO_CATEGORIES, CATEGORY_*,
  DEFAULT_SUBCATEGORY, GECKO_FORMAT_VERSION, GECKO_PREPROCESSED_VERSION,
  RESOURCE_TYPE_LIBRARY, FRAME_ADDRESS_NONE, FRAME_INLINE_DEPTH_ROOT,
  PROCESS_TYPE_MAIN, STACKWALK_DISABLED — integer constants that encode
  the internal Gecko profile format; not part of user-facing API
- sampling/heatmap_collector.pyi: removed _TemplateLoader, _TreeBuilder,
  _HtmlRenderer — private classes (underscore-prefixed) used only
  internally for HTML rendering
- sampling/opcode_utils.pyi: removed base_opcode, variant_opcode — these
  are local variables inside get_opcode_info, not module-level attributes;
  the stubs were incorrect
- sampling/sample.pyi: removed MIN_SAMPLES_FOR_TUI — internal threshold
  constant for TUI display logic
- sampling/_sync_coordinator.pyi: removed CoordinatorError, ArgumentError,
  SyncError, TargetError — all exceptions live in a private module
  (_sync_coordinator) and are only raised and caught internally; never
  exposed through any public module

Kept despite `dead` flagging:

- tracing/__init__.pyi: dump_stats, snapshot_stats, runcall — public
  methods on the exported Profile class; standard profiling API
- sampling/binary_collector.pyi: get_stats, collect_failed_sample —
  public methods; collect_failed_sample is part of the Collector interface
  called in sample.py
- sampling/binary_reader.pyi: get_stats, convert_binary_to_format —
  public method and public utility function
- sampling/string_table.pyi: intern, get_string, get_strings — core API
  of StringTable
- sampling/live_collector/display.pyi: get_dimensions, refresh, redraw,
  get_input, set_nodelay, has_colors, init_color_pair, get_color_pair,
  get_attr, find_text — abstract methods on DisplayInterface define the
  ABC contract; concrete implementations on CursesDisplay and MockDisplay
  must also be stubbed
- sampling/live_collector/trend_tracker.pyi: set_enabled, get_trend —
  public methods on public class
- sampling/live_collector/widgets.pyi: format_rate_with_units,
  draw_thread_status, draw_finished_banner, draw_stats_rows,
  render_filter_input_prompt — public methods on public widget classes
- sampling/live_collector/collector.pyi: increment_status_flag,
  as_status_dict, collect_failed_sample — public methods on ThreadData
  dataclass and LiveStatsCollector

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Judgement calls — removed:

- sampling/_css_utils.pyi: deleted — private module (_css_utils) used
  only internally by stack_collector and heatmap_collector; get_combined_css
  is not part of any public API
- sampling/_format_utils.pyi: deleted — private module (_format_utils)
  used only internally; fmt is a simple formatting helper not intended
  for external use
- sampling/_sync_coordinator.pyi: deleted — private module only containing
  main() -> Never, which is a subprocess entry point never called externally
- sampling/_child_monitor.pyi: removed get_child_pids and is_python_process
  — private utility functions used only inside _child_monitor itself;
  ChildProcessMonitor kept because it is re-exported from cli.pyi
- sampling/heatmap_collector.pyi: removed re-exports of get_combined_css
  and fmt — these are private implementation imports, not part of
  HeatmapCollector's public API
- sampling/stack_collector.pyi: removed re-export of get_combined_css —
  same reason as above
- sampling/sample.pyi: removed re-export of fmt — private formatting
  helper leaked into a public module's stub

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
live_collector/__init__.pyi re-exported ~30 TUI layout/color constants
(MICROSECONDS_PER_SECOND, MIN_TERMINAL_WIDTH, WIDTH_THRESHOLD_*, COLOR_PAIR_*,
COL_WIDTH_*, etc.) in __all__. Nothing outside the live_collector package
ever imports them — only LiveStatsCollector and MockDisplay are actually
used by callers in the source and tests.

Judgement calls — removed:

- live_collector/__init__.pyi: stripped all constant imports and __all__
  entries down to the 10 public classes (LiveStatsCollector, DisplayInterface,
  CursesDisplay, MockDisplay, and the Widget subclasses) — the constants
  are internal TUI dimensions and curses color pair IDs, not user-facing API
- live_collector/constants.pyi: removed DISPLAY_UPDATE_HZ, COL_WIDTH_NSAMPLES,
  COL_SPACING, COL_WIDTH_SAMPLE_PCT, COL_WIDTH_TIME, MIN_AVAILABLE_SPACE —
  these became unreferenced once the __init__ re-exports were dropped; they
  are not imported by any other stub in the package

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

johnslavik and others added 3 commits April 20, 2026 06:24
Guiding principle: stubs should only contain what users of the profiling
API would actually import or tab-complete. The live_collector subpackage
is primarily a TUI display component; its internal widget/display
architecture is not part of any user-facing API.

Judgement calls — removed:

- live_collector/widgets.pyi: deleted — Widget, ProgressBarWidget,
  HeaderWidget, TableWidget, FooterWidget, HelpWidget, OpcodePanel are
  internal TUI rendering components; nobody writing code against the
  profiling API would import them
- live_collector/trend_tracker.pyi: deleted — TrendTracker is an internal
  widget state helper used only inside the TUI rendering loop
- live_collector/constants.pyi: deleted — TUI layout dimensions and curses
  color pair IDs (COLOR_PAIR_*, WIDTH_THRESHOLD_*, MIN_TERMINAL_*, etc.)
  have no meaning outside the live_collector implementation
- live_collector/display.pyi: removed CursesDisplay (internal curses
  wrapper) and MockDisplay (test helper only referenced in tests, not
  production API); kept DisplayInterface because it is the type of the
  display= parameter on LiveStatsCollector.__init__
- live_collector/collector.pyi: removed all constant imports (COLOR_PAIR_*,
  MICROSECONDS_PER_SECOND, MIN_TERMINAL_*, etc.) that were leaked
  implementation imports not used in any type annotations; removed widget
  imports (HeaderWidget, TableWidget, etc.) and TrendTracker; removed
  ThreadData dataclass (internal per-sample accumulator); stripped
  LiveStatsCollector of TUI-state attributes (stdscr, selected_row,
  scroll_offset, paused, show_help, filter_*, view_mode,
  current_thread_index, per_thread_data, *_widget, display_update_interval_sec,
  thread_status_counts, gc_frame_samples, opcode_stats) and internal
  methods (simplify_path, process_frames, init_curses, cleanup_curses)
- live_collector/__init__.pyi: stripped __all__ and re-exports to just
  LiveStatsCollector and DisplayInterface

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
It was removed in the first cleanup commit as "re-exported only
internally", but pstats_collector.pyi legitimately imports it from
sampling.constants — where it is actually defined in the source.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Judgement calls, per the principle: will people want to import these
symbols in their code that uses the profiling API?

profiling.tracing
-----------------
- Remove Profile.snapshot_stats(): not in __all__, not in docs. It is
  an internal implementation step called by create_stats(); users call
  create_stats() directly.
- Remove module-level main(): not in __all__, not in docs. It is the
  CLI entry point wired up by the (already-deleted) __main__.py stub.
- Keep dump_stats, runcall, create_stats, print_stats: all explicitly
  documented in the profiling.tracing module reference.
- Keep private _T, _P, _Label aliases: needed internally by the stub
  to express the runcall() overload and stats dict type.

profiling.sampling — collector submodule stubs
-----------------------------------------------
- Remove normalize_location, extract_lineno, filter_internal_frames
  from collector.pyi: implementation helpers used inside collector
  subclasses; not in sampling.__all__, not documented. Users subclass
  Collector with collect() and export() only.
- Remove DEFAULT_LOCATION and THREAD_STATUS_* re-exports from
  collector.pyi: leaked from constants.py; import from
  profiling.sampling.constants if needed.
- Remove get_opcode_mapping from opcode_utils.pyi: internal mapping
  used to build opcode-name look-up tables; not documented, never
  imported by user code.
- Remove re-exports of filter_internal_frames, format_opcode,
  get_opcode_info from gecko_collector.pyi: imported for internal
  use, not part of GeckoCollector's public contract.
- Strip GeckoCollector internal tracking attributes (has_gil_start,
  no_gil_start, on_cpu_start, etc.): 20+ state dicts tracking
  GIL/CPU marker transitions; purely implementation detail. Users
  call __init__ → collect → export.
- Remove FileStats, TreeNode, get_python_path_info,
  extract_module_name from heatmap_collector.pyi: internal data
  structures and path helpers for the HTML generation pipeline; not
  in sampling.__all__, not documented.
- Strip HeatmapCollector internal state counters (line_samples,
  file_samples, call_graph, etc.): implementation detail of the
  line-level sample accumulation.
- Remove re-exports of extract_lineno, normalize_location,
  format_opcode, get_opcode_info from heatmap_collector.pyi: same
  pattern — imported for internal use only.
- Remove re-exports of extract_lineno, get_opcode_mapping,
  StringTable from stack_collector.pyi.
- Strip CollapsedStackCollector.stack_counter and
  FlamegraphCollector internal state (thread_status_counts,
  per_thread_stats, etc.): sample accumulation internals.
- Strip DiffFlamegraphCollector.baseline_binary_path attribute:
  constructor parameter, not a user-facing attribute.
- Remove extract_lineno, MICROSECONDS_PER_SECOND, PROFILING_MODE_CPU
  re-exports from pstats_collector.pyi.
- Strip PstatsCollector internal state (result, stats, callers,
  skip_idle attributes): implementation detail of sample aggregation.
- Remove GeckoCollector, PstatsCollector, CollapsedStackCollector,
  FlamegraphCollector re-exports from binary_reader.pyi: imported
  internally by binary_reader.py, not part of BinaryReader's API.

profiling.sampling — sample and cli modules
-------------------------------------------
- Strip sample.pyi of all re-exports (BinaryCollector, Collector,
  all five PROFILING_MODE_* constants, GeckoCollector,
  HeatmapCollector, LiveStatsCollector, PstatsCollector,
  CollapsedStackCollector, FlamegraphCollector): sample.py imports
  these for internal use; they are not part of sample's public
  interface and are already accessible from their own modules.
- Remove unwinder: Incomplete from SampleProfiler: internal C
  extension handle, not user-accessible.
- Strip cli.pyi to main() only: cli.py is the CLI implementation;
  all the symbols it re-exported (ChildProcessMonitor, BinaryCollector,
  BinaryReader, 6 SORT_MODE constants) belong to their own modules.
- Delete _child_monitor.pyi: private module (_-prefixed); previously
  kept because cli.pyi re-exported ChildProcessMonitor, but now that
  cli.pyi is stripped there is no public re-export, and users have
  no reason to import from profiling.sampling._child_monitor directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

1 similar comment
@github-actions
Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

mypy (https://github.com/python/mypy)
  .../projects/mypy/mypy/__init__.py: error: INTERNAL ERROR -- Please try using mypy master on GitHub:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant