Ctrl + k

or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
pypipkg:pypi/typeshed-client@2.8.x

docs

index.md
tile.json

tessl/pypi-typeshed-client

tessl install tessl/pypi-typeshed-client@2.8.4

A library for accessing stubs in typeshed.

parsing-stubs.mddocs/reference/

Parsing Stubs

Functions for parsing stub files into structured dictionaries that map names to their AST nodes. The parser handles exports, imports, overloads, and conditional definitions based on Python version and platform checks.

Quick Reference

FunctionReturn TypeCan Return None?
get_stub_names(module_name)Optional[NameDict]Yes
parse_ast(ast, ctx, module_name)NameDictNo
get_import_star_names(module_name, *, search_context)Optional[list[str]]Yes
get_dunder_all_from_info(info)Optional[list[str]]Yes

Capabilities

Get Stub Names

Returns a dictionary of all names defined in a module's stub.

def get_stub_names(module_name: str, *, search_context: Optional[SearchContext] = None) -> Optional[NameDict]:
    """
    Given a module name, return a dictionary of names defined in that module.

    Args:
        module_name (str): Module name as a dotted string (e.g., 'typing', 'collections.abc')
        search_context (SearchContext, optional): Context for finding and parsing stubs. Uses default if None.

    Returns:
        NameDict: Dictionary mapping name strings to NameInfo objects, or None if module not found

    Example:
        names = get_stub_names('collections')
        if names:
            for name, info in names.items():
                if info.is_exported:
                    print(f"{name}: exported={info.is_exported}")
    """

Return Value Details:

  • Returns NameDict (dict[str, NameInfo]) mapping names to their information
  • Returns None if module not found
  • Dictionary keys are unqualified names (e.g., 'OrderedDict', not 'collections.OrderedDict')
  • Includes both exported and private names
  • Empty dict if module exists but has no names

Performance: Parses stub file; results should be cached if used multiple times

Parse AST

Parses an AST into a dictionary of names, handling conditional definitions and imports.

def parse_ast(
    ast: ast.AST,
    search_context: SearchContext,
    module_name: ModulePath,
    *,
    is_init: bool = False,
    file_path: Optional[Path] = None,
    is_py_file: bool = False
) -> NameDict:
    """
    Parse an AST into a dictionary of names.

    Args:
        ast (ast.AST): AST to parse
        search_context (SearchContext): Context for parsing (version, platform)
        module_name (ModulePath): Module path as tuple of strings
        is_init (bool, optional): Whether this is an __init__ file. Default: False
        file_path (Path, optional): Path to source file for error reporting
        is_py_file (bool, optional): Whether this is a .py file (vs .pyi). Default: False

    Returns:
        NameDict: Dictionary mapping names to NameInfo objects

    Example:
        from typeshed_client import get_stub_ast, get_search_context, ModulePath
        from typeshed_client.parser import parse_ast

        ast_tree = get_stub_ast('typing')
        ctx = get_search_context()
        names = parse_ast(ast_tree, ctx, ModulePath(('typing',)))
    """

Return Value Details:

  • Always returns NameDict (never None)
  • Empty dict if AST has no definitions
  • Evaluates conditional blocks based on search_context.version and search_context.platform
  • Handles @overload decorators by creating OverloadedName objects
  • Resolves imports into ImportedName objects

Raises: InvalidStub if raise_on_warnings=True and parser encounters issues

NameDict Structure

A NameDict is a dictionary mapping name strings to NameInfo objects:

from typeshed_client import get_stub_names

names = get_stub_names('typing')
# Structure:
# names: NameDict = {
#     'function_name': NameInfo(
#         name='function_name',
#         is_exported=True,
#         ast=<ast.FunctionDef>,
#         child_nodes=None
#     ),
#     'ClassName': NameInfo(
#         name='ClassName',
#         is_exported=True,
#         ast=<ast.ClassDef>,
#         child_nodes={
#             'method_name': NameInfo(...),
#             '__init__': NameInfo(...),
#         }
#     ),
#     '_private_func': NameInfo(
#         name='_private_func',
#         is_exported=False,
#         ast=<ast.FunctionDef>,
#         child_nodes=None
#     ),
# }

Key Points:

  • Dictionary keys match NameInfo.name field
  • Values are always NameInfo objects
  • May be empty dict (but not None for parse_ast())
  • child_nodes is recursive NameDict for class members

Name Export Rules

Names are considered exported (part of the public interface) if:

  1. The name does not start with an underscore, OR
  2. The name is listed in __all__

Special Cases:

  • Names starting with single underscore (_name) are private unless in __all__
  • Dunder names (__name__) follow same rules
  • Names imported with from module import * respect source module's __all__

Examples:

# In stub file:
# public_name: int  # is_exported=True (no underscore)
# _private_name: int  # is_exported=False (starts with _)
# __all__ = ['public_name', '_exported_private']
# _exported_private: int  # is_exported=True (in __all__)

names = get_stub_names('module')
if names:
    print(f"public_name exported: {names['public_name'].is_exported}")  # True
    print(f"_private_name exported: {names['_private_name'].is_exported}")  # False
    print(f"_exported_private exported: {names['_exported_private'].is_exported}")  # True

Handling Overloaded Functions

When a stub contains multiple definitions of the same name (typically from @overload decorator), the parser creates an OverloadedName:

from typeshed_client import get_stub_names, OverloadedName
import ast

# Example stub content:
# @overload
# def func(x: int) -> int: ...
# @overload
# def func(x: str) -> str: ...
# def func(x): ...

names = get_stub_names('module_with_overloads')
if names and 'func' in names:
    func_info = names['func']
    if isinstance(func_info.ast, OverloadedName):
        print(f"func has {len(func_info.ast.definitions)} definitions")
        for i, defn in enumerate(func_info.ast.definitions):
            if isinstance(defn, ast.FunctionDef):
                print(f"  Definition {i}: {defn.name}")
                # Get arguments
                args = [arg.arg for arg in defn.args.args]
                print(f"    Args: {args}")
                # Get return type
                if defn.returns:
                    ret = ast.unparse(defn.returns)
                    print(f"    Returns: {ret}")

Key Points:

  • OverloadedName.definitions is a list of AST nodes
  • Last definition is usually the implementation (without @overload)
  • Each definition is a separate ast.FunctionDef node
  • Can mix ast.FunctionDef and ImportedName if overload is re-exported

Handling Imports

When a name is imported from another module, the parser creates an ImportedName:

from typeshed_client import get_stub_names, ImportedName

# Example stub: from collections import OrderedDict

names = get_stub_names('example_module')
if names and 'OrderedDict' in names:
    info = names['OrderedDict']
    if isinstance(info.ast, ImportedName):
        print(f"Imported from: {'.'.join(info.ast.module_name)}")
        print(f"Name: {info.ast.name}")  # 'OrderedDict'
        print(f"Exported: {info.is_exported}")

For module imports:

from typeshed_client import get_stub_names, ImportedName

# Example stub: import collections

names = get_stub_names('example_module')
if names and 'collections' in names:
    info = names['collections']
    if isinstance(info.ast, ImportedName):
        print(f"Module import: {'.'.join(info.ast.module_name)}")
        print(f"Name: {info.ast.name}")  # None for module imports

Key Points:

  • ImportedName.module_name is a ModulePath (tuple of strings)
  • ImportedName.name is None for module imports (import foo)
  • ImportedName.name is the imported name for specific imports (from foo import bar)
  • Use Resolver to follow imports to their source

Handling Conditional Definitions

The parser evaluates conditions based on sys.version_info and sys.platform:

from typeshed_client import get_search_context, get_stub_names

# Example stub:
# if sys.version_info >= (3, 10):
#     def new_function() -> None: ...
# if sys.platform == 'win32':
#     def windows_only() -> None: ...

# Parse with Python 3.10
ctx_310 = get_search_context(version=(3, 10))
names_310 = get_stub_names('example_module', search_context=ctx_310)
if names_310:
    print(f"Has new_function: {'new_function' in names_310}")  # True

# Parse with Python 3.9
ctx_39 = get_search_context(version=(3, 9))
names_39 = get_stub_names('example_module', search_context=ctx_39)
if names_39:
    print(f"Has new_function: {'new_function' in names_39}")  # False

# Parse with Windows platform
ctx_win = get_search_context(platform='win32')
names_win = get_stub_names('example_module', search_context=ctx_win)
if names_win:
    print(f"Has windows_only: {'windows_only' in names_win}")  # True

Supported conditional expressions:

  • sys.version_info comparisons: >=, >, <, <=, ==, !=
  • sys.platform comparisons: ==, !=
  • TYPE_CHECKING: Always evaluated as True in stubs
  • MYPY: Always evaluated as False
  • Boolean operations: and, or, not
  • Tuple comparisons: sys.version_info >= (3, 10)

Examples:

# In stub file:
# if sys.version_info >= (3, 10):
#     new_feature: int
# if sys.version_info < (3, 11):
#     deprecated_feature: int
# if sys.platform == "win32":
#     windows_feature: int
# if sys.platform in ("linux", "darwin"):
#     unix_feature: int
# if TYPE_CHECKING:
#     forward_ref: SomeClass  # Always included

Usage Examples

Basic Name Extraction

from typeshed_client import get_stub_names

names = get_stub_names('typing')

if names:
    # Print all exported names
    exported = [name for name, info in names.items() if info.is_exported]
    print(f"Exported names: {len(exported)}")
    print(f"First 5: {exported[:5]}")
    
    # Print all private names
    private = [name for name, info in names.items() if not info.is_exported]
    print(f"Private names: {len(private)}")
    
    # Count by type
    functions = sum(1 for info in names.values() 
                    if isinstance(info.ast, ast.FunctionDef))
    classes = sum(1 for info in names.values() 
                  if isinstance(info.ast, ast.ClassDef))
    print(f"Functions: {functions}, Classes: {classes}")
else:
    print("Module not found")

Analyzing Function Signatures

from typeshed_client import get_stub_names
import ast

names = get_stub_names('typing')

if names:
    # Find function definitions
    print("Functions in typing module:")
    for name, info in names.items():
        if isinstance(info.ast, ast.FunctionDef):
            # Get arguments
            args = [arg.arg for arg in info.ast.args.args]
            arg_str = ', '.join(args)
            
            # Get return type annotation
            ret_str = "Any"
            if info.ast.returns:
                ret_str = ast.unparse(info.ast.returns)
            
            exported = "๐Ÿ”“" if info.is_exported else "๐Ÿ”’"
            print(f"{exported} {name}({arg_str}) -> {ret_str}")

Examining Class Methods

from typeshed_client import get_stub_names
import ast

names = get_stub_names('typing')

if names and 'TypedDict' in names:
    class_info = names['TypedDict']
    if isinstance(class_info.ast, ast.ClassDef):
        print(f"Class: {class_info.name}")
        print(f"Exported: {class_info.is_exported}")
        
        # Get base classes
        if class_info.ast.bases:
            bases = [ast.unparse(base) for base in class_info.ast.bases]
            print(f"Bases: {', '.join(bases)}")
        
        # Examine methods
        if class_info.child_nodes:
            print(f"\nMethods ({len(class_info.child_nodes)}):")
            for method_name, method_info in class_info.child_nodes.items():
                exported = "public" if method_info.is_exported else "private"
                if isinstance(method_info.ast, ast.FunctionDef):
                    print(f"  {method_name}: function ({exported})")
                elif isinstance(method_info.ast, (ast.Assign, ast.AnnAssign)):
                    print(f"  {method_name}: attribute ({exported})")
        else:
            print("No child nodes available")

Handling Overloaded Functions

from typeshed_client import get_stub_names, OverloadedName
import ast

names = get_stub_names('typing')

if names:
    # Find overloaded functions
    print("Overloaded functions:")
    for name, info in names.items():
        if isinstance(info.ast, OverloadedName):
            print(f"\n{name} ({len(info.ast.definitions)} overloads):")
            for i, defn in enumerate(info.ast.definitions, 1):
                if isinstance(defn, ast.FunctionDef):
                    # Get signature
                    args = [arg.arg for arg in defn.args.args]
                    arg_str = ', '.join(args) or 'no args'
                    
                    # Get return type
                    ret_str = "Any"
                    if defn.returns:
                        ret_str = ast.unparse(defn.returns)
                    
                    # Check for @overload decorator
                    is_overload = any(
                        isinstance(dec, ast.Name) and dec.id == 'overload'
                        for dec in defn.decorator_list
                    )
                    marker = "@overload" if is_overload else "implementation"
                    
                    print(f"  {i}. [{marker}] ({arg_str}) -> {ret_str}")

Tracing Imports

from typeshed_client import get_stub_names, ImportedName

names = get_stub_names('collections')

if names:
    # Find imported names
    imports = {}
    local_definitions = {}
    
    for name, info in names.items():
        if isinstance(info.ast, ImportedName):
            import_info = info.ast
            if import_info.name:
                source = f"{'.'.join(import_info.module_name)}.{import_info.name}"
            else:
                source = f"{'.'.join(import_info.module_name)} (module)"
            imports[name] = source
        else:
            local_definitions[name] = type(info.ast).__name__
    
    print(f"Imported names ({len(imports)}):")
    for name, source in sorted(imports.items())[:10]:
        print(f"  {name} <- {source}")
    
    print(f"\nLocal definitions ({len(local_definitions)}):")
    for name, ast_type in sorted(local_definitions.items())[:10]:
        print(f"  {name}: {ast_type}")

Version-Specific Parsing

from typeshed_client import get_search_context, get_stub_names

versions = [(3, 9), (3, 10), (3, 11), (3, 12)]
results = {}

print("Parsing typing module across Python versions:")
for version in versions:
    ctx = get_search_context(version=version)
    names = get_stub_names('typing', search_context=ctx)
    
    if names:
        results[version] = set(names.keys())
        exported = sum(1 for info in names.values() if info.is_exported)
        print(f"Python {version[0]}.{version[1]}: {len(names)} names ({exported} exported)")

# Compare which names are available
if len(results) >= 2:
    versions_sorted = sorted(results.keys())
    v_oldest = versions_sorted[0]
    v_newest = versions_sorted[-1]
    
    names_only_in_newest = results[v_newest] - results[v_oldest]
    if names_only_in_newest:
        print(f"\nNames added after Python {v_oldest[0]}.{v_oldest[1]}:")
        for name in sorted(names_only_in_newest):
            print(f"  + {name}")
    
    names_removed = results[v_oldest] - results[v_newest]
    if names_removed:
        print(f"\nNames removed:")
        for name in sorted(names_removed):
            print(f"  - {name}")

Custom AST Parsing

from typeshed_client import get_search_context, ModulePath
from typeshed_client.parser import parse_ast
import ast

# Create a custom AST
code = """
def foo(x: int) -> str:
    '''A function that does something.'''
    ...

class Bar:
    '''A simple class.'''
    def method(self) -> None: ...
    attribute: int = 42

_private_var: int = 42

__all__ = ['foo', 'Bar']
"""
tree = ast.parse(code)

# Parse it with typeshed_client
ctx = get_search_context()
names = parse_ast(tree, ctx, ModulePath(('mymodule',)))

print(f"Names found: {list(names.keys())}")  # ['foo', 'Bar', '_private_var', '__all__']

# Check export status
print("\nExport status:")
for name, info in names.items():
    status = "exported" if info.is_exported else "private"
    ast_type = type(info.ast).__name__
    print(f"  {name}: {status} ({ast_type})")

# Examine class structure
if 'Bar' in names:
    bar_info = names['Bar']
    if bar_info.child_nodes:
        print(f"\nBar class members:")
        for member_name, member_info in bar_info.child_nodes.items():
            print(f"  {member_name}: {type(member_info.ast).__name__}")

Error Handling

InvalidStub Exception

The parser raises InvalidStub exception for malformed stubs.

class InvalidStub(Exception):
    """Raised when the parser encounters invalid stub syntax."""
    def __init__(self, message: str, file_path: Optional[Path] = None) -> None: ...

When Raised:

  • Invalid stub syntax that doesn't match Python grammar
  • When raise_on_warnings=True in SearchContext and parser encounters warnings
  • Unsupported or malformed type stub constructs
  • Issues parsing __all__ definitions

Attributes:

  • message: Description of the error (from str(exception))
  • file_path: Path to problematic stub file (if provided, may be None)

Usage:

from typeshed_client import get_search_context, ModulePath
from typeshed_client.parser import parse_ast, InvalidStub
import ast

try:
    # Parse potentially invalid AST
    tree = ast.parse("x + y")  # Expression, not a module definition
    ctx = get_search_context()
    names = parse_ast(tree, ctx, ModulePath(('test',)))
except InvalidStub as e:
    print(f"Invalid stub: {e}")
    if hasattr(e, 'args') and len(e.args) > 1:
        print(f"File: {e.args[1]}")

By default, warnings are logged rather than raised. To raise on warnings:

from typeshed_client import get_search_context, get_stub_names
from typeshed_client.parser import InvalidStub

ctx = get_search_context(raise_on_warnings=True)

modules = ['typing', 'collections', 'some_malformed_module']
for module in modules:
    try:
        names = get_stub_names(module, search_context=ctx)
        if names:
            print(f"โœ“ {module}: {len(names)} names")
        else:
            print(f"โœ— {module}: Not found")
    except InvalidStub as e:
        print(f"โœ— {module}: Parsing failed: {e}")

Additional Parser Functions

Get Import Star Names

Returns the list of names that would be imported by from module import *.

def get_import_star_names(
    module_name: str,
    *,
    search_context: SearchContext,
    file_path: Optional[Path] = None
) -> Optional[list[str]]:
    """
    Return list of names that would be imported by 'from module import *'.

    Args:
        module_name (str): Module name as a dotted string
        search_context (SearchContext): Context for finding and parsing stubs
        file_path (Path, optional): Path to source file for error reporting

    Returns:
        list[str]: List of names that would be imported, or None if module not found

    Example:
        from typeshed_client.parser import get_import_star_names
        from typeshed_client import get_search_context

        ctx = get_search_context()
        names = get_import_star_names('typing', search_context=ctx)
        print(names)  # ['Any', 'Union', 'Optional', ...]
    """

Behavior:

  • If __all__ is defined, returns its contents
  • Otherwise, returns all names not starting with underscore
  • Returns None if module not found
  • Returns empty list if module has no exportable names

Usage Example:

from typeshed_client.parser import get_import_star_names
from typeshed_client import get_search_context

ctx = get_search_context()

# Get names from typing module
typing_names = get_import_star_names('typing', search_context=ctx)
if typing_names:
    print(f"'from typing import *' would import {len(typing_names)} names")
    print(f"First 10: {typing_names[:10]}")
else:
    print("Module not found")

Get Dunder All From Info

Extracts the __all__ list from a NameInfo object.

def get_dunder_all_from_info(
    info: NameInfo,
    file_path: Optional[Path] = None
) -> Optional[list[str]]:
    """
    Extract the __all__ list from a NameInfo object.

    Args:
        info (NameInfo): NameInfo object for __all__
        file_path (Path, optional): Path to source file for error reporting

    Returns:
        list[str]: List of names in __all__, or None if cannot be extracted

    Example:
        from typeshed_client import get_stub_names
        from typeshed_client.parser import get_dunder_all_from_info

        names = get_stub_names('typing')
        if names and '__all__' in names:
            all_list = get_dunder_all_from_info(names['__all__'])
            print(all_list)  # ['Any', 'Union', 'Optional', ...]
    """

Behavior:

  • Extracts list of strings from __all__ definition
  • Returns None if __all__ is not a simple list of strings
  • Handles both Assign and AnnAssign nodes
  • Returns None if extraction fails

Usage Example:

from typeshed_client import get_stub_names, get_search_context
from typeshed_client.parser import get_import_star_names, get_dunder_all_from_info

ctx = get_search_context()

# Method 1: Get names that would be imported with 'from typing import *'
star_names = get_import_star_names('typing', search_context=ctx)
if star_names:
    print(f"Star import would get {len(star_names)} names")

# Method 2: Get __all__ directly from parsed names
names = get_stub_names('typing')
if names and '__all__' in names:
    all_list = get_dunder_all_from_info(names['__all__'])
    if all_list:
        print(f"__all__ contains {len(all_list)} names")
        # Should be equal
        if star_names and set(star_names) == set(all_list):
            print("โœ“ Both methods return same names")
    else:
        print("Could not extract __all__")

Advanced Patterns

Pattern: Filter by AST Node Type

from typeshed_client import get_stub_names
import ast

def filter_by_node_type(module_name: str, node_type):
    """Get all names of a specific AST node type."""
    names = get_stub_names(module_name)
    if not names:
        return []
    
    result = []
    for name, info in names.items():
        if isinstance(info.ast, node_type):
            result.append(name)
    
    return result

# Find all classes in typing module
classes = filter_by_node_type('typing', ast.ClassDef)
print(f"Classes in typing ({len(classes)}): {classes[:5]}...")

# Find all functions
functions = filter_by_node_type('typing', ast.FunctionDef)
print(f"Functions in typing ({len(functions)}): {functions[:5]}...")

# Find all type aliases (assignments)
aliases = filter_by_node_type('typing', (ast.Assign, ast.AnnAssign))
print(f"Type aliases in typing ({len(aliases)}): {aliases[:5]}...")

Pattern: Analyze Type Annotations

from typeshed_client import get_stub_names
import ast

def analyze_annotations(module_name: str):
    """Analyze type annotations in a module."""
    names = get_stub_names(module_name)
    if not names:
        return {}
    
    annotations = {}
    for name, info in names.items():
        if isinstance(info.ast, ast.AnnAssign):
            # Variable with type annotation
            if info.ast.annotation:
                ann_str = ast.unparse(info.ast.annotation)
                annotations[name] = ann_str
        elif isinstance(info.ast, ast.FunctionDef):
            # Function with return annotation
            if info.ast.returns:
                ret_str = ast.unparse(info.ast.returns)
                annotations[name] = f"-> {ret_str}"
    
    return annotations

# Get annotated items
typed_items = analyze_annotations('typing')
if typed_items:
    print(f"Found {len(typed_items)} annotated items")
    for item, type_str in list(typed_items.items())[:10]:
        print(f"  {item}: {type_str}")

Pattern: Extract Class Hierarchies

from typeshed_client import get_stub_names
import ast

def get_class_hierarchy(module_name: str):
    """Extract class inheritance information."""
    names = get_stub_names(module_name)
    if not names:
        return {}
    
    hierarchy = {}
    for name, info in names.items():
        if isinstance(info.ast, ast.ClassDef):
            bases = []
            for base in info.ast.bases:
                bases.append(ast.unparse(base))
            hierarchy[name] = {
                'bases': bases,
                'exported': info.is_exported,
                'methods': list(info.child_nodes.keys()) if info.child_nodes else []
            }
    
    return hierarchy

# Get inheritance info
classes = get_class_hierarchy('collections')
if classes:
    print(f"Found {len(classes)} classes")
    for cls, details in list(classes.items())[:5]:
        print(f"\n{cls}:")
        if details['bases']:
            print(f"  Inherits from: {', '.join(details['bases'])}")
        print(f"  Exported: {details['exported']}")
        print(f"  Methods: {len(details['methods'])}")

Pattern: Find Deprecated APIs

from typeshed_client import get_stub_names
import ast

def find_deprecated(module_name: str):
    """Find deprecated functions/classes by looking for deprecation decorators."""
    names = get_stub_names(module_name)
    if not names:
        return []
    
    deprecated = []
    for name, info in names.items():
        if isinstance(info.ast, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            for decorator in info.ast.decorator_list:
                # Check for @deprecated or @deprecate decorator
                is_deprecated = False
                if isinstance(decorator, ast.Name):
                    if 'deprecat' in decorator.id.lower():
                        is_deprecated = True
                elif isinstance(decorator, ast.Call):
                    if isinstance(decorator.func, ast.Name):
                        if 'deprecat' in decorator.func.id.lower():
                            is_deprecated = True
                
                if is_deprecated:
                    deprecated.append(name)
                    break
    
    return deprecated

# Find deprecated items in multiple modules
for module in ['typing', 'collections', 'asyncio']:
    deprecated_items = find_deprecated(module)
    if deprecated_items:
        print(f"{module}: {deprecated_items}")

Pattern: Compare Module APIs

from typeshed_client import get_stub_names

def compare_modules(module1: str, module2: str):
    """Compare the public APIs of two modules."""
    names1 = get_stub_names(module1)
    names2 = get_stub_names(module2)
    
    if not names1 or not names2:
        return None
    
    exported1 = {name for name, info in names1.items() if info.is_exported}
    exported2 = {name for name, info in names2.items() if info.is_exported}
    
    return {
        'common': exported1 & exported2,
        'only_in_first': exported1 - exported2,
        'only_in_second': exported2 - exported1,
        'total_first': len(exported1),
        'total_second': len(exported2),
    }

# Compare collections and collections.abc
result = compare_modules('collections', 'collections.abc')
if result:
    print(f"collections: {result['total_first']} exported names")
    print(f"collections.abc: {result['total_second']} exported names")
    print(f"Common: {len(result['common'])} names")
    print(f"Only in collections: {len(result['only_in_first'])}")
    print(f"Only in collections.abc: {len(result['only_in_second'])}")
    
    if result['common']:
        print(f"\nSome common names: {list(result['common'])[:5]}")

Pattern: Extract Generic Parameters

from typeshed_client import get_stub_names
import ast

def extract_generic_params(module_name: str):
    """Extract generic type parameters from classes."""
    names = get_stub_names(module_name)
    if not names:
        return {}
    
    generics = {}
    for name, info in names.items():
        if isinstance(info.ast, ast.ClassDef):
            # Look for Generic base class
            for base in info.ast.bases:
                base_str = ast.unparse(base)
                if 'Generic[' in base_str or base_str == 'Generic':
                    generics[name] = base_str
                    break
    
    return generics

# Find generic classes
generic_classes = extract_generic_params('typing')
if generic_classes:
    print(f"Found {len(generic_classes)} generic classes:")
    for cls, base in list(generic_classes.items())[:10]:
        print(f"  {cls}: {base}")

Best Practices for Agents

Return Value Handling

  1. Always check for None: get_stub_names() returns None when a module is not found
# CORRECT
names = get_stub_names('module')
if names is None:
    print("Module not found")
elif len(names) == 0:
    print("Module found but empty")
else:
    print(f"Found {len(names)} names")

# INCORRECT
names = get_stub_names('module')
for name in names:  # TypeError if names is None
    print(name)
  1. Check AST node types: Use isinstance() to determine what kind of AST node a name represents
# CORRECT
from typeshed_client import ImportedName, OverloadedName
import ast

info = names['somename']
if isinstance(info.ast, ImportedName):
    print("It's an import")
elif isinstance(info.ast, OverloadedName):
    print("It's overloaded")
elif isinstance(info.ast, ast.FunctionDef):
    print("It's a function")
elif isinstance(info.ast, ast.ClassDef):
    print("It's a class")
  1. Handle special cases: ImportedName and OverloadedName are not regular AST nodes
# CORRECT - check for special types first
from typeshed_client import ImportedName, OverloadedName
import ast

if isinstance(info.ast, (ImportedName, OverloadedName)):
    # Handle special types
    pass
elif isinstance(info.ast, ast.AST):
    # Handle regular AST nodes
    pass

Export Status

  1. Consider export status: Check is_exported to distinguish public from private APIs
# CORRECT - filter by export status
public_names = {name: info for name, info in names.items() 
                if info.is_exported}
private_names = {name: info for name, info in names.items() 
                 if not info.is_exported}

print(f"Public: {len(public_names)}, Private: {len(private_names)}")

Child Nodes

  1. Handle child_nodes: For classes, check child_nodes to access methods and attributes
# CORRECT
info = names['SomeClass']
if isinstance(info.ast, ast.ClassDef) and info.child_nodes:
    for method_name, method_info in info.child_nodes.items():
        print(f"Method: {method_name}")
else:
    print("No child nodes available")

# INCORRECT
info = names['SomeClass']
for method in info.child_nodes:  # AttributeError if child_nodes is None
    print(method)

Version Awareness

  1. Version awareness: Parse with appropriate SearchContext for version-specific behavior
# CORRECT - explicit version handling
versions = [(3, 9), (3, 10), (3, 11)]
for version in versions:
    ctx = get_search_context(version=version)
    names = get_stub_names('typing', search_context=ctx)
    if names:
        print(f"Python {version}: {len(names)} names")

Error Handling

  1. Error handling: Wrap parsing in try-except when using raise_on_warnings=True
# CORRECT
from typeshed_client.parser import InvalidStub

ctx = get_search_context(raise_on_warnings=True)
try:
    names = get_stub_names('module', search_context=ctx)
    if names:
        print(f"Success: {len(names)} names")
except InvalidStub as e:
    print(f"Parsing failed: {e}")

Context Reuse

  1. Reuse context: Create SearchContext once and reuse for multiple parsing operations
# CORRECT - efficient
ctx = get_search_context(version=(3, 11))
modules = ['typing', 'collections', 'asyncio']
for module in modules:
    names = get_stub_names(module, search_context=ctx)
    # Process names

AST Attributes

  1. Inspect carefully: Not all AST node attributes are always present; check before accessing
# CORRECT
if isinstance(info.ast, ast.FunctionDef):
    # Check for return annotation
    if info.ast.returns:
        ret_type = ast.unparse(info.ast.returns)
        print(f"Returns: {ret_type}")
    else:
        print("No return annotation")
    
    # Check for decorators
    if info.ast.decorator_list:
        for dec in info.ast.decorator_list:
            print(f"Decorator: {ast.unparse(dec)}")

# INCORRECT
if isinstance(info.ast, ast.FunctionDef):
    ret_type = ast.unparse(info.ast.returns)  # May be None

Overload Handling

  1. Process overloads correctly: When encountering OverloadedName, iterate through all definitions
# CORRECT
from typeshed_client import OverloadedName
import ast

if isinstance(info.ast, OverloadedName):
    for i, defn in enumerate(info.ast.definitions):
        if isinstance(defn, ast.FunctionDef):
            print(f"Overload {i}: {ast.unparse(defn.args)}")
        elif isinstance(defn, ImportedName):
            print(f"Overload {i}: imported")