Table of Contents


1. Introduction

Curiosity often leads to some of the most interesting projects. After reading about PEP 514 in the Astral UV documentation, I discovered that Python installations on Windows are registered in the Windows Registry. Intrigued, I tried the sample code from PEP 514, only to find it lacking. This post documents my journey to build a more robust, flexible Python script for reading Windows Registry values. Hopefully this may be useful for someone.

Windows RegEdit Application showing Python user entries


2. Background: What is the Windows Registry?

The Windows Registry is a hierarchical database that stores configuration settings and options for the operating system and installed applications in the form of Name:Value pairs. For Python developers, it’s especially relevant because Python installations are registered here (as per PEP 514), making it possible to discover and manage multiple Python versions programmatically.

Structure of the Windows Registry

The Windows Registry is similar to a file-system. The top-level nodes (hives) are known as root-keys, and each top-level root-key contains subkeys (like folders) and values (like files).

  graph TD
    A[HKEY_CLASSES_ROOT]
    B[HKEY_CURRENT_USER]
    C[HKEY_LOCAL_MACHINE]
    D[HKEY_USERS]
    E[HKEY_CURRENT_CONFIG]
    F[Subkeys & Values]

    A --> F
    B --> F
    C --> F
    D --> F
    E --> F
Root Key Purpose
HKEY_CLASSES_ROOT Stores information about registered applications, file associations, and COM objects.
HKEY_CURRENT_USER Contains settings and preferences for the currently logged-in user.
HKEY_LOCAL_MACHINE Holds configuration data for the local computer, including hardware and software.
HKEY_USERS Contains user-specific configuration for all users on the system.
HKEY_CURRENT_CONFIG Stores information about the current hardware profile used at system startup.

A path is made from a root-key, keys, subkeys, and values. For example:

Hive (root key): HKEY_CURRENT_USER  
└── Key: Software  
    └── Subkey: Python  
        └── Subkey: Astral  
            └── Subkey: CPython3.13.5  
                └── Value: InstallPath
  • A key (or subkey) is like a folder—it can contain more subkeys and also values.
    • CPython3.13.5 is a subkey. It can contain more subkeys (like InstallPath) and/or values (like Default, ExecutablePath, etc.).
  • A value is a Name:Value pair stored inside a key. It does not contain further subkeys or values.
    • InstallPath can be either a subkey (if it contains more subkeys/values) or a value (if it is just a Name:Value pair).

The term “hive” is often used in documentation for the Windows Registry and seems to be a term specific to Windows, used by Microsoft to describe the files and top-level sections of the registry.


3. The Problem with Sample Code: Lessons from PEP 514

When I first tried the two blocks of sample code from PEP 514, I expected it to work out of the box. Instead, it returned incomplete or misleading results:

PEP 514 Sample Code #1 PEP 514 Sample Code #2

From looking at the Windows RegEdit application I knew this code was not reporting the correct values.

Reading the Python WinReg module documentation, we can get the code samples working by replacing the winreg.QueryValue() function call with the recommended winreg.QueryValueEx() function call and unpacking the returned value. This now gives:

PEP 514 Sample Code Updated

That simple change, and removing the access mask to open the key (sample #1 only), has gotten us further. The fields are now looking better for those handled.

The Modified PEP 514 Sample Code is on my GitHub.

I can see there is still more information we can access for the Python entries not handled by the sample code though.


4. Exploring the Registry: Exporting and Analysing Data

To better understand the registry’s structure, I exported sections of it using the Windows regedit.exe application and analysed the resulting files in the REPL. This helped me see the variety of data types and key/value patterns present.

The regedit.exe application does not allow you to export (dump) the whole registry as a single text file. You have to export the registry piecemeal, which in itself is a bit of a warning sign that there is a lot of data.

For my system, there can be a large number of keys to traverse depending on the root HKEY chosen:

Filename File Size Line Count Key Count
regdump_HKEY_CURRENT_CONFIG.txt 3.65 (KB) 58 12
regdump_HKEY_CURRENT_USER.txt 73.45 (MB) 611842 18402
regdump_HKEY_USERS.txt 100.40 (MB) 898276 35469
regdump_HKEY_CLASSES_ROOT.txt 122.69 (MB) 1931975 201571
regdump_HKEY_LOCAL_MACHINE.txt 553.31 (MB) 7558804 529983
Totals 849.85 (MB) 11,000,955 785,437

I’m not looking for much, just some idea of what we have. The following code is an example of what was run in the REPL as I was exploring the data, but it is runnable as a script on my GitHub. Python is quite capable of handling all these files simultaneously:

Example: Analysing Exported Registry Files

# file_analyse.py
import fileinput
from pprint import pprint

tuple_of_files = (
    "regdump_HKEY_CLASSES_ROOT.txt",
    "regdump_HKEY_CURRENT_CONFIG.txt",
    "regdump_HKEY_CURRENT_USER.txt",
    "regdump_HKEY_LOCAL_MACHINE.txt",
    "regdump_HKEY_USERS.txt",
)
# There are some 'locale' characters that will not work for UTF-8
with fileinput.input(files=tuple_of_files, encoding="utf-16") as fid:
    data = list(fid)

# What Can we find out about Keys
keynames = []
for this_line in data:
    if this_line.startswith("Key Name:"):
        # Caution: Keys can have whitespace
        keynames.append(this_line.split("Key Name:")[1].strip())
print(f"Key Count: {len(keynames)}\n")
pprint(keynames[:12])  # Print 1st 12
# Likely the longest and deepest key-paths are the same...
longest_path = max(keynames, key=len)
print(f"\nLongest key-path is {len(longest_path)} characters.")
print(f"With {len(longest_path.split('\\'))} keys in it:\n")
print(longest_path)  # The actual key-path

deepest_path = max(keynames, key=lambda x: len(x.split("\\")))
print(f"\nDeepest key-path is {len(deepest_path)} characters.")
print(f"With {len(deepest_path.split('\\'))} keys in it:\n")
print(deepest_path)  # The actual key-path

# So as we thought, Key Names are like paths
# i.e., the path is made up of keys and subkeys
... # other code available on GitHub

Example summary of my registry key statistics

Line Count: 11000955
Lines Blank: 2017686
...which accounts for 18.34% of all lines
...removed blank lines, which has reduced line count by 18.34%
Updated Line Count: 8983269
Key Count: 785437
Longest key-path is 407 characters.
With 28 keys in it
Deepest key-path is 407 characters.
Duplicate Keys = False

HKEYS listed:
{'HKEY_CLASSES_ROOT', 'HKEY_CURRENT_CONFIG' 'HKEY_CURRENT_USER', 'HKEY_LOCAL_MACHINE', 'HKEY_USERS'}

Types found:
{'REG_BINARY', 'REG_DWORD', 'REG_EXPAND_SZ', 'REG_FULL_RESOURCE_DESCRIPTOR',
 'REG_MULTI_SZ', 'REG_NONE', 'REG_QWORD', 'REG_RESOURCE_LIST',
 'REG_RESOURCE_REQUIREMENTS_LIST', 'REG_SZ', 'REG_UNKNOWN'}

The most common Class Name entries (Name, Count):
[('<NO CLASS>\n', 783641), ('REG_SZ\n', 1090), ('Shell\n', 694),
 ('Network ComputerName\n', 2), ('1aa506ff\n', 2), ('adda2636\n', 2),
 ('5c15e5ed\n', 2), ('2c356ba6\n', 2), ('DynDRootClass\n', 2)]

There are a lot of keys (>780k) and the longest key-path is long (28 keys or 407 characters). There is one value Type not defined in the Python WinReg module, which is REG_UNKNOWN. This appears when the Type is just an undefined hex value. We can either print the hex value or follow suit and use REG_UNKNOWN.

Class Name entries appear in the exported text files, but not in the RegEdit application itself. I think this is the RegEdit exporter being clever by cross-referencing the Class, ClassID and ClassGuidName’ entries defined elsewhere in the registry.
Update: I’ve subsequently confirmed that I cannot access anything named ‘Class Name’ in the registry with the Python WinReg module, and therefore ignoring these is a safe thing to do.

I’m only dealing with 64-bit Windows, so I’m not worrying about accessing 32-bit applications. You may need to investigate further if you have to worry about 32-bit access – see Accessing an Alternate Registry View for more information.

The simplest Python WinReg code I could write to read a value:

import winreg

key_handle = winreg.OpenKeyEx(
    winreg.HKEY_CURRENT_USER,        # Root key
    "Software\\Python\\PythonCore",  # Subkey
    access=winreg.KEY_READ,          # Default is KEY_READ
)

# Read known named value for the given subkey
# Ignore value_type, we do nothing with it
value, value_type = winreg.QueryValueEx(key_handle, "DisplayName",)
print(f"SubKey value: DisplayName='{value}'")

# "key_handle" object closed automatically when the object is destroyed
# Alternatively use "winreg.CloseKey()", or better still a Context Manager

There is a winreg.QueryInfoKey() function that returns the number of sub-keys and number of values for the passed key-path. You could then read each value (in a for-loop) with the winreg.QueryValueEx() function.

Alternatively there is the preferred winreg.EnumKey() and winreg.EnumValue() functions that retrieve the name/value of one subkey each time they are called. These are typically called repeatedly until an OSError exception is raised, indicating no more values are available.

The latter is preferable for traversing a key-path.

5. Building a Better Registry Reader in Python

I set out to write a script that can:

  • Read any registry key or value, not just Python-related ones
  • Traverse subkeys recursively
  • Handle errors gracefully..somewhat
  • Be easily extended for new use cases

We will define some design caveats first:

  • We will only be connecting locally, otherwise see winreg.ConnectRegistry().
  • Read Only, I don’t want to write to the Registry (Danger!)
  • 64-bit only
  • Print results to the console only, can redirect to file on the console if needed
  • Use the REG_UNKNOWN type for undefined type values
  • Command Line (CLI) use, but also programmatically callable
  • One root-key at a time on CLI – can programmatically call more if needed
  • Allow specific subkeys to be ignored - helps overcome permission issues and errors in the registry

All the source code is available in the GitHub Repository: winreg_read.


Getting Keys and Values

I like the sample code method of using yield. Not keen on having the counter, so we can use recursion instead:

# DO NOT DO THIS
def get_keys(hkey, path, index=0):
    try:
        yield winreg.EnumKey(winreg.OpenKey(hkey, path), index)
        yield from get_keys(hkey, path, index+1)
    except OSError as err:
        pass

Okay, bad idea #1 complete! Turns out this causes a RecursionError.

What I did not realise was that Python has a recursion limit (sys.getrecursionlimit()), and using recursion in the Keys and Values functions meant we exceeded that limit (RecursionError exception), even for moderately-short traversals. You can change the recursion limit (sys.setrecursionlimit(n)), but I was not keen on doing so. So I went back to the counter method.
This was also the time I became concerned about key handles leaking for deep traversals.

The code is perhaps less Pedantic Pythonic, but it’s probably clearer to understand with the counter method anyway!

# USE THIS for get_keys() and get_values()
def get_keys(hkey, path):
    """Yield all subkey names under the given HKey and sub-key path."""
    try:
        # Explicitly close handles, otherwise risk of leaks for large traversals
        with winreg.OpenKey(hkey, path) as key:
            index = 0
            while True:
                try:
                    yield winreg.EnumKey(key, index)
                    index += 1
                except OSError:  # Expected when no more keys to yield
                    break

    except FileNotFoundError as err:
        msg = f"\n{path} is not a valid path"
        raise FileNotFoundError(msg) from err
    except PermissionError as err:
        print(f"{err}: Permission Error: you may need to run the script as Admin.")

We’ve added some exception protection for invalid paths, and some of the Registry may not be accessible to the user.

Traversing Subkeys via Recursion

We want:

  flowchart TD
    A[1: Function Entry - Traverse Key-Path] --> B[2: Get Values for Key-Path]
    B --> C{3: For each Subkey in get_keys}
    C -- No more Subkeys --> CC[4: Continue]
    C-- For each Subkey -->D[5: Build Subkey-Path]
    D--> E[6: Get Values for Subkey-Path]
    E --> F{7: For each Sub-Subkey-Path in get_keys}
    F -- No more Sub-Subkeys --> FF[8: Continue]
    F -- For each Sub-Subkey --> G[9: Build Sub-Subkey-Path]
    G --> H[10: Recursive call: Traverse Sub-Subkey-Path]
    H --> A

Printing Results

I only care about printing to the console. The RegEdit application’s exported text files are close to what I want, but perhaps with just some tweaking:

Full_Path\Copyable_for_RegEdit
    TYPE      NAME      VALUE

Full_Path\Copyable_for_RegEdit\Plus_Subkey
    TYPE      NAME      VALUE

...and so on...

We’ve seen some of the paths can be huge (>400 characters for my system), the empty line between paths will help identify blocks of subkey values when the console wraps these multi-key paths.

You can redirect the console output to a file and remove line-wrap in your editor if the wrapping bothers you.

We can set some constants for the TYPE-NAME-VALUE column spacing.

Arguments with ArgParse

ArgParse is quick, simple and all we need. It can be extended as needed:

usage: winreg_read.py [-h] [-e [EXCLUDE ...]] HKey Key-Path

Traverse Windows Registry and Print the Values

positional arguments:
  HKey                  Enter HKey, e.g. 'HKEY_CURRENT_USER'
  Key-Path              Subkey-Path to traverse from, 
                        e.g. 'Software\python'

options:
  -h, --help            show this help message and exit
  -e, --exclude [EXCLUDE ...]
                        List of Key-Paths to exclude from being traversed, 
                        i.e., ignored. Expected to be '-e 'path1' 'pathn'

Issues Found

  • Non-CONSTANT Type Values:
    Some Type entries are numbers and not one of the Type constants. For those I use the REG_UNKNOWN type.

  • Forward Slash in Key Name:
    I wanted to normalise slashes from the user, using something like os.path.join(*path.title().replace(r"/", "\\").split("\\")). But, at least one key in my Windows registry has forward slashes in it, and for the Windows registry you cannot have backslashes in a key name, e.g.
    HKEY_CURRENT_USER\Software\Classes\AppUserModelId\C:/ProgramData/ASUS/AsusSurvey/AsusSurvey.exe
    The key is C:/ProgramData/ASUS/AsusSurvey/AsusSurvey.exe.
    So instead, depend on the user knowing what they are doing [sic].

  • Non-Standard Characters:
    There are some locale names that threw a UnicodeDecodeError. I guess they did not match my console locale setting. Best to change your console default. For PowerShell, chcp 65001 (UTF-8) worked for me…or just don’t traverse those keys.

  • Permission errors:
    When you open the RegEdit application it opens in Admin mode. There may be some keys you will need Admin permission to access. I catch and continue for those, but you may need to run the script as admin to access all the keys.

  • Windows Registry Errors:
    There are some entries in my Windows registry that do not work, even in the RegEdit application. Nothing I can do except fix the Windows Registry itself, or add those keys/paths to the exclude argument.

Final Tidy

It’s important to know when to stop.
You can spend forever tweaking and twiddling. Use tools like Ruff to take the majority of decision-making out of your hands.


6. Practical Example: Using the Script

Let’s see the script in action. Suppose you want to list all Python installations or inspect a custom registry key. Here’s how you might use the script:

Command-line Examples

uv run winreg_read.py "HKEY_CURRENT_USER" "SOFTWARE\Python"
uv run winreg_read.py "HKEY_CURRENT_USER" "SOFTWARE\Python" > HKCU_python.txt

uv run winreg_read.py HKEY_LOCAL_MACHINE SOFTWARE\Microsoft\WindowsUpdate
uv run winreg_read.py HKEY_USERS ''  # Not recommended for all HKey types
uv run winreg_read.py "HKEY_CLASSES_ROOT" "" -e "WOW6432Node" "zune"  # No commas
uv run winreg_read.py "HKEY_CURRENT_CONFIG" "System" -e "System\CurrentControlSet\Enum" "System\CurrentControlSet\SERVICES"  # No commas

Example Console Output

Script Examples

Example 1

Traverse a given HKEY and subkey-path:

# import winreg  # Instead, use the import via 'winreg_read'import 
import winreg_read

hkey = winreg_read.winreg.HKEY_CURRENT_USER
key_path = r"software\python"

winreg_read.traverse_winreg_for_values(hkey, key_path, None)

Traverse a given HKEY and Subkey-Path

Example 2

Display the Python versions available from all manufacturers for the current user:

import winreg_read as wrr

hkey = wrr.winreg.HKEY_CURRENT_USER
key_path = r"software\python"

# List Python versions for each available manufacturer
for this_subkey in wrr.get_keys(hkey, key_path):
    print(f"\n{this_subkey.title()} Python Versions:")
    for this_key in wrr.get_keys(hkey, '\\'.join([key_path, this_subkey])):
        print(f"\t{this_key}")

List Python versions for each available manufacturer

…and then if we wanted to see details of a particular Python version:

wrr.traverse_winreg_for_values(hkey, "software\\python\\Astral\\CPython3.14.0b4", None)

…see details of a particular Python version


7. Key Takeaways

  • Python is progressing fast, sample code becomes outdated quickly
  • The Windows Registry is a powerful resource for automation and discovery
  • Python’s winreg module can be used for much more than just reading Python installation information

Reading a Registry Value

  • Use winreg.OpenKey() to open a key
  • Use winreg.QueryValueEx() to read a value

Writing to the Registry

  • Always back up the registry before making changes
  • Use winreg.CreateKey() or winreg.OpenKey() with write access
  • Use winreg.SetValueEx() to set a value

Deleting a Registry Key or Value

  • Use winreg.DeleteKey() or winreg.DeleteValue() for cleanup or automation

8. Try It Yourself

Try out the script, experiment with different keys, and share your findings or improvements.


* AI has been used for checking spelling, grammar and technical details on this Blog post.