Skip to content

pylsp/jedi False Positive Type Warnings #228

@lubomir

Description

@lubomir

Caution

AI generated report follows

Summary

Python LSP Server (pylsp) with jedi is producing false positive type warnings when working with the productmd library, incorrectly inferring types as Unknown | None when the actual runtime type is str or dict.

Environment

  • pylsp version: 1.13.1
  • Python version: 3.14.3
  • productmd version: 1.50 (development branch commit b719ac380fa94a3c68093479957d3ae1aab75c6b)
  • Active pylsp plugins: jedi_completion, jedi_definition, jedi_hover, jedi_references, jedi_rename, jedi_signature_help, jedi_symbols, jedi_type_definition

Root Cause

The productmd library has no type annotations:

  • No py.typed marker file
  • No .pyi stub files
  • No __annotations__ on classes
  • No return type annotations on methods

pylsp/jedi is attempting to infer types from runtime behavior but is incorrectly determining that certain attributes could be None when they are guaranteed to be specific types after initialization.

Issue 1: False Positive on ComposeInfo.compose.id

LSP Warning

ERROR [312:29] Argument of type "Unknown | None" cannot be assigned to parameter "compose_id" of type "str" in function "print_summary"
  Type "Unknown | None" is not assignable to type "str"
    "None" is not assignable to "str"

Code

from productmd.composeinfo import ComposeInfo

def create_compose_info(args) -> ComposeInfo:
    ci = ComposeInfo()
    ci.release.name = args.release_name
    ci.release.short = args.release_short
    ci.release.version = args.release_version
    ci.release.type = args.release_type
    ci.release.is_layered = args.release_is_layered
    ci.compose.date = args.compose_date
    ci.compose.type = args.compose_type
    ci.compose.respin = args.compose_respin
    if args.compose_label:
        ci.compose.label = args.compose_label
    ci.compose.id = ci.create_compose_id()  # This returns str
    return ci

def print_summary(args, compose_id: str) -> None:
    # ... implementation ...
    pass

def main() -> int:
    args = parse_args()
    compose_info = create_compose_info(args)
    compose_id = compose_info.compose.id  # LSP thinks this is Unknown | None
    
    # LSP ERROR: compose_id inferred as Unknown | None, but it's actually str
    print_summary(args, compose_id)  # <-- False positive warning here

Actual Runtime Behavior

>>> from productmd.composeinfo import ComposeInfo
>>> ci = ComposeInfo()
>>> ci.compose.id
None  # Before calling create_compose_id()
>>> ci.release.name = "Test"
>>> ci.release.short = "TEST"
>>> ci.release.version = "1.0"
>>> ci.release.type = "ga"
>>> ci.compose.date = "20260311"
>>> ci.compose.type = "production"
>>> ci.compose.respin = 0
>>> ci.compose.id = ci.create_compose_id()
>>> type(ci.compose.id)
<class 'str'>
>>> ci.compose.id
'TEST-1.0-20260311.0'

Expected: After calling ci.create_compose_id() and assigning it to ci.compose.id, the type should be inferred as str, not Unknown | None.

Actual: pylsp/jedi infers the type as Unknown | None because ci.compose.id starts as None before initialization.

Issue 2: False Positive on VariantPaths Dictionary Attributes

LSP Warnings

ERROR [68:35] Cannot access attribute "repository" for class "VariantPaths"
  Attribute "repository" is unknown
ERROR [74:35] Cannot access attribute "debug_repository" for class "VariantPaths"
  Attribute "debug_repository" is unknown
ERROR [80:31] Cannot access attribute "source_repository" for class "VariantPaths"
  Attribute "source_repository" is unknown

Code

from productmd.composeinfo import ComposeInfo, Variant

ci = ComposeInfo()
variant = Variant(ci)
variant.id = "BaseOS"
variant.uid = "BaseOS"
variant.name = "BaseOS"
variant.type = "variant"
variant.arches = {"x86_64"}
ci.variants.add(variant)

# LSP ERROR: Claims these attributes don't exist
variant.paths.repository["x86_64"] = "https://example.com/repo"           # <-- Warning
variant.paths.debug_repository["x86_64"] = "https://example.com/debug"     # <-- Warning
variant.paths.source_repository["src"] = "https://example.com/source"      # <-- Warning

Actual Runtime Behavior

>>> from productmd.composeinfo import Variant, ComposeInfo
>>> ci = ComposeInfo()
>>> v = Variant(ci)
>>> type(v.paths.repository)
<class 'dict'>
>>> type(v.paths.debug_repository)
<class 'dict'>
>>> type(v.paths.source_repository)
<class 'dict'>
>>> v.paths.repository
{}
>>> v.paths.debug_repository
{}
>>> v.paths.source_repository
{}
>>> # These all work perfectly at runtime
>>> v.paths.repository["x86_64"] = "https://example.com/repo"
>>> v.paths.debug_repository["x86_64"] = "https://example.com/debug"
>>> v.paths.source_repository["src"] = "https://example.com/source"
>>> v.paths.repository
{'x86_64': <productmd.common.Location object at 0x...>}

Expected: pylsp/jedi should recognize that VariantPaths has repository, debug_repository, and source_repository attributes that are dictionaries.

Actual: pylsp/jedi claims these attributes don't exist on VariantPaths class, despite them being present and functioning correctly at runtime.

Impact

These false positive warnings:

  1. Create noise in the development environment, making it harder to spot real issues
  2. Reduce trust in the LSP's type checking capabilities
  3. Waste developer time investigating non-existent problems
  4. May cause CI failures if type checking is enforced strictly

Reproduction Case

#!/usr/bin/env python3
"""Minimal reproduction case for pylsp/jedi type inference issues with productmd."""

from productmd.composeinfo import ComposeInfo, Variant


def create_compose_info() -> ComposeInfo:
    """Create a ComposeInfo object - demonstrates issue #1."""
    ci = ComposeInfo()
    ci.release.name = "Test"
    ci.release.short = "TEST"
    ci.release.version = "1.0"
    ci.release.type = "ga"
    ci.release.is_layered = False
    ci.compose.date = "20260311"
    ci.compose.type = "production"
    ci.compose.respin = 0
    
    # create_compose_id() returns str, but LSP thinks ci.compose.id is Unknown | None
    ci.compose.id = ci.create_compose_id()
    
    return ci


def process_compose_id(compose_id: str) -> None:
    """Process a compose ID - expects str parameter."""
    print(f"Processing: {compose_id}")


def setup_variant_paths() -> None:
    """Set up variant paths - demonstrates issue #2."""
    ci = ComposeInfo()
    variant = Variant(ci)
    variant.id = "BaseOS"
    variant.uid = "BaseOS"
    variant.name = "BaseOS"
    variant.type = "variant"
    variant.arches = {"x86_64"}
    ci.variants.add(variant)
    
    # LSP claims these attributes don't exist, but they work at runtime
    variant.paths.repository["x86_64"] = "https://example.com/repo"
    variant.paths.debug_repository["x86_64"] = "https://example.com/debug"
    variant.paths.source_repository["src"] = "https://example.com/source"


def main() -> None:
    """Main function demonstrating both issues."""
    # Issue #1: False positive - compose_id is str, not Unknown | None
    compose_info = create_compose_info()
    compose_id = compose_info.compose.id
    process_compose_id(compose_id)  # <-- LSP ERROR: False positive type warning
    
    # Issue #2: False positive - attributes exist, LSP claims they don't
    setup_variant_paths()  # <-- LSP ERROR: Claims attributes don't exist


if __name__ == "__main__":
    main()

Additional Context

These issues are not caused by the modification in the development branch, the same problems are most likely happening on master too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions