How to Get the File ID of a Windows File in Python
On Windows, every file on an NTFS volume has a unique File ID, i.e. a numeric identifier that works similarly to an inode number on Linux/Unix systems. File IDs are useful for tracking files regardless of renames or moves within the same volume, building file indexing systems, or implementing deduplication logic.
In this guide, you'll learn how to retrieve a Windows file's unique ID using Python and how to reverse the process to find a file's path from its ID.
Understanding Windows File IDs
A File ID is a unique identifier assigned by the NTFS file system to every file and directory on a volume. Key characteristics include:
- File IDs are unique within a volume (for example, the
C:drive). - They persist even when a file is renamed or moved within the same volume.
- They are represented as hexadecimal values (for example,
0x0000000000000000000100000000589).
Windows provides the fsutil command-line utility to query and work with file IDs. We'll invoke this utility from Python using os.popen() or the subprocess module.
Getting a File ID from a File Path
The fsutil file queryfileid command retrieves the unique ID for a file at a given path. You can call it from Python using os.popen():
from os import popen
# Path to the file (use a raw string to handle backslashes)
file_path = r"C:\Users\YourName\Desktop\example.txt"
# Query the file ID using fsutil
output = popen(f'fsutil file queryfileid "{file_path}"').read().strip()
print(output)
Output:
File ID is 0x00000000000000000001000000000589
How it works:
fsutil file queryfileidis a Windows command that returns the NTFS file ID for the specified path.os.popen()runs the command in a subprocess and captures its output..read().strip()retrieves the output text and removes trailing whitespace.
fsutil requires administrator privileges for some operations. If you get an "Access is denied" error, run your Python script or terminal as Administrator.
Getting a File Path from a File ID
The reverse operation, finding a file's path from its ID, uses the fsutil file queryfilenamebyid command:
from os import popen
# The file ID obtained previously
file_id = "0x00000000000000000001000000000589"
# Query the file path using the file ID (specify the volume)
output = popen(f'fsutil file queryfilenamebyid C:\\ {file_id}').read().strip()
print(output)
Output:
A random link name to this file is \\?\C:\Users\YourName\Desktop\example.txt
The actual file path is C:\Users\YourName\Desktop\example.txt.
Using subprocess Instead of os.popen() (Recommended)
The subprocess module provides better error handling, security, and control over the process. It's the recommended alternative to os.popen():
import subprocess
def get_file_id(file_path):
"""Get the NTFS file ID for a given file path."""
result = subprocess.run(
["fsutil", "file", "queryfileid", file_path],
capture_output=True,
text=True
)
if result.returncode != 0:
raise FileNotFoundError(
f"Could not get file ID: {result.stderr.strip()}"
)
# Extract the hex ID from the output
# Output format: "File ID is 0x..."
output = result.stdout.strip()
file_id = output.split("is")[-1].strip()
return file_id
def get_path_from_id(volume, file_id):
"""Get the file path from a file ID on the specified volume."""
result = subprocess.run(
["fsutil", "file", "queryfilenamebyid", volume, file_id],
capture_output=True,
text=True
)
if result.returncode != 0:
raise FileNotFoundError(
f"Could not resolve file ID: {result.stderr.strip()}"
)
# Extract the path from the output
output = result.stdout.strip()
# Output: "A random link name to this file is \\?\C:\..."
path = output.split("\\\\?\\")[-1] if "\\\\?\\" in output else output
return path
# Usage
file_path = r"C:\Users\YourName\Desktop\example.txt"
try:
fid = get_file_id(file_path)
print(f"File ID: {fid}")
resolved_path = get_path_from_id("C:\\", fid)
print(f"Resolved path: {resolved_path}")
except FileNotFoundError as e:
print(f"Error: {e}")
Output:
File ID: 0x00000000000000000001000000000589
Resolved path: C:\Users\YourName\Desktop\example.txt
subprocess.run() over os.popen()?- Error handling:
subprocess.run()providesreturncode,stderr, andstdoutseparately, making it easy to detect and handle failures. - Security: Passing arguments as a list avoids shell injection vulnerabilities.
- Modern API:
os.popen()is considered a legacy interface;subprocessis the recommended replacement.
Using the Windows API Directly with ctypes
For a more robust approach that doesn't depend on command-line tools, you can use the Windows API through ctypes:
import ctypes
from ctypes import wintypes
import os
def get_file_id_by_handle(file_path):
"""Get the file ID using Windows API (GetFileInformationByHandle)."""
# Open the file to get a handle
GENERIC_READ = 0x80000000
FILE_SHARE_READ = 0x00000001
OPEN_EXISTING = 3
handle = ctypes.windll.kernel32.CreateFileW(
file_path,
GENERIC_READ,
FILE_SHARE_READ,
None,
OPEN_EXISTING,
0,
None,
)
if handle == -1:
raise FileNotFoundError(f"Cannot open file: {file_path}")
try:
# Define the BY_HANDLE_FILE_INFORMATION structure
class BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
_fields_ = [
("dwFileAttributes", wintypes.DWORD),
("ftCreationTime", wintypes.FILETIME),
("ftLastAccessTime", wintypes.FILETIME),
("ftLastWriteTime", wintypes.FILETIME),
("dwVolumeSerialNumber", wintypes.DWORD),
("nFileSizeHigh", wintypes.DWORD),
("nFileSizeLow", wintypes.DWORD),
("nNumberOfLinks", wintypes.DWORD),
("nFileIndexHigh", wintypes.DWORD),
("nFileIndexLow", wintypes.DWORD),
]
info = BY_HANDLE_FILE_INFORMATION()
success = ctypes.windll.kernel32.GetFileInformationByHandle(
handle, ctypes.byref(info)
)
if not success:
raise OSError("GetFileInformationByHandle failed")
# Combine high and low parts into the full file ID
file_id = (info.nFileIndexHigh << 32) | info.nFileIndexLow
return file_id
finally:
ctypes.windll.kernel32.CloseHandle(handle)
# Usage
file_path = r"C:\Users\YourName\Desktop\example.txt"
try:
file_id = get_file_id_by_handle(file_path)
print(f"File ID: {file_id}")
print(f"File ID (hex): 0x{file_id:016X}")
except (FileNotFoundError, OSError) as e:
print(f"Error: {e}")
This approach bypasses the command line entirely and queries the file system directly through the Windows API.
Common Mistake: Forgetting to Handle Missing Files
If the specified file doesn't exist, fsutil returns an error message instead of a file ID. Without proper error handling, your code may silently return incorrect results.
Wrong approach: no error handling.
from os import popen
file_path = r"C:\nonexistent\file.txt"
output = popen(f'fsutil file queryfileid "{file_path}"').read()
print(output) # Prints an error message, not a file ID
Output:
Error: The system cannot find the file specified.
If your code tries to parse this as a file ID, it will produce garbage.
Correct approach: validate the result.
import subprocess
def get_file_id_safe(file_path):
"""Get file ID with proper error handling."""
result = subprocess.run(
["fsutil", "file", "queryfileid", file_path],
capture_output=True,
text=True
)
if result.returncode != 0 or "Error" in result.stdout:
return None
return result.stdout.strip().split("is")[-1].strip()
file_id = get_file_id_safe(r"C:\nonexistent\file.txt")
if file_id:
print(f"File ID: {file_id}")
else:
print("File not found or access denied.")
Output:
File not found or access denied.
Understanding the fsutil Commands
| Command | Purpose | Example |
|---|---|---|
fsutil file queryfileid <path> | Get a file's unique ID from its path | Returns 0x... hex value |
fsutil file queryfilenamebyid <volume> <id> | Get a file's path from its ID | Returns the file's location |
fsutil and Windows File IDs are Windows-only features. This code will not work on macOS or Linux. For cross-platform file identification, consider using os.stat() which returns st_ino (inode number) on Unix systems and a file index on Windows.
import os
stat = os.stat(r"C:\Users\YourName\Desktop\example.txt")
print(f"File index (inode): {stat.st_ino}")
Conclusion
Retrieving Windows File IDs in Python is straightforward using either command-line utilities or the Windows API:
fsutilwithsubprocess.run()is the recommended approach for most use cases. It's simple, handles errors well, and avoids shell injection risks.os.popen()works but lacks proper error handling and is considered a legacy API.- Windows API via
ctypesprovides the most robust solution without command-line dependencies. - Always handle missing files and permission errors gracefully to avoid silent failures.
- Remember that File IDs are volume-specific: the same ID on different drives refers to different files.