How to Create a Simple Hosts-Based Ad Blocker in Python
Windows console application with Enable/Disable controls
Introduction
In this tutorial, we will create a small Windows console application in Python that can enable or disable a hosts-based domain blocker.
The program will:
- Use Y to enable the blocker
- Use N to disable it
- Use Q to quit without changing anything
- Request administrator rights through the normal Windows UAC prompt
- Add only its own marked section to the Windows hosts file
- Remove that section again without deleting unrelated hosts entries
- Flush the Windows DNS cache after every change
Important: The hosts file affects the entire computer. Review the domain list before running the script. Blocking service-related domains can break websites or applications, and you should respect the terms of the services you use.
Requirements
- Windows 10 or Windows 11
- Python 3.10 or newer
- Administrator access
- A text editor such as Visual Studio Code or Notepad++
Check whether Python is installed:
If that command is not found, install Python from

and enable
Add Python to PATH during installation.
Step 1: Create the Python file
Create a file named
spotlock_console.py and insert this code:
Code:
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ctypes
import os
import subprocess
import sys
from pathlib import Path
APP_NAME = "SpotLock"
START_MARKER = "# Begin SpoTLock AdBlock"
END_MARKER = "# End SpoTLock AdBlock"
# Replace or extend these example entries with domains you want to block.
BLOCKED_DOMAINS = [
"ads.example.com",
"tracking.example.com",
]
def get_hosts_path() -> Path:
windows_dir = os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows"
return Path(windows_dir) / "System32" / "drivers" / "etc" / "hosts"
def is_admin() -> bool:
try:
return bool(ctypes.windll.shell32.IsUserAnAdmin())
except Exception:
return False
def relaunch_as_admin(action: str, pause: bool = False) -> None:
argument_parts = []
if not getattr(sys, "frozen", False):
argument_parts.append(str(Path(__file__).resolve()))
argument_parts.append(f"--{action}")
if pause:
argument_parts.append("--pause")
arguments = subprocess.list2cmdline(argument_parts)
result = ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, arguments, None, 1
)
if result <= 32:
raise RuntimeError("Windows could not start the elevated process.")
def read_hosts(path: Path) -> str:
if not path.exists():
return ""
raw = path.read_bytes()
for encoding in ("utf-8-sig", "utf-8", "cp1252"):
try:
return raw.decode(encoding)
except UnicodeDecodeError:
continue
return raw.decode("utf-8", errors="replace")
def write_hosts(path: Path, content: str) -> None:
path.write_text(content, encoding="utf-8", newline="\n")
def remove_managed_block(content: str) -> str:
lower_content = content.lower()
start = lower_content.find(START_MARKER.lower())
while start >= 0:
end = lower_content.find(END_MARKER.lower(), start)
if end < 0:
content = content[:start]
else:
after = end + len(END_MARKER)
while after < len(content) and content[after] in "\r\n":
after += 1
content = content[:start].rstrip() + "\n" + content[after:]
lower_content = content.lower()
start = lower_content.find(START_MARKER.lower())
return content.rstrip() + ("\n" if content.strip() else "")
def blocker_is_enabled(path: Path) -> bool:
content = read_hosts(path).lower()
return START_MARKER.lower() in content and END_MARKER.lower() in content
def flush_dns() -> None:
subprocess.run(
["ipconfig", "/flushdns"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
def enable_blocker() -> None:
path = get_hosts_path()
original = remove_managed_block(read_hosts(path)).rstrip()
lines = []
if original:
lines.extend([original, ""])
lines.append(START_MARKER)
for domain in sorted({item.strip().lower() for item in BLOCKED_DOMAINS if item.strip()}):
lines.append(f"0.0.0.0 {domain}")
lines.extend([END_MARKER, ""])
write_hosts(path, "\n".join(lines))
flush_dns()
def disable_blocker() -> None:
path = get_hosts_path()
write_hosts(path, remove_managed_block(read_hosts(path)))
flush_dns()
def run_action(action: str, pause_after_elevation: bool = False) -> int:
if action in {"enable", "disable"} and not is_admin():
print("Administrator rights are required. Opening the Windows UAC prompt...")
relaunch_as_admin(action, pause_after_elevation)
return 0
if action == "enable":
enable_blocker()
print("SpotLock is enabled.")
elif action == "disable":
disable_blocker()
print("SpotLock is disabled.")
elif action == "status":
state = "enabled" if blocker_is_enabled(get_hosts_path()) else "disabled"
print(f"SpotLock is currently {state}.")
else:
raise ValueError(f"Unknown action: {action}")
return 0
def interactive() -> int:
state = "enabled" if blocker_is_enabled(get_hosts_path()) else "disabled"
print(f"{APP_NAME} Console")
print(f"Current status: {state}")
print("\n[Y] Enable\n[N] Disable\n[Q] Quit\n")
choice = input("Select an option: ").strip().lower()
if choice in {"y", "yes"}:
return run_action("enable", pause_after_elevation=True)
if choice in {"n", "no"}:
return run_action("disable", pause_after_elevation=True)
if choice in {"q", "quit", "exit", ""}:
print("No changes were made.")
return 0
print("Unknown option.")
return 1
def main() -> int:
parser = argparse.ArgumentParser(description="SpotLock console application")
actions = parser.add_mutually_exclusive_group()
actions.add_argument("--enable", action="store_true")
actions.add_argument("--disable", action="store_true")
actions.add_argument("--status", action="store_true")
parser.add_argument("--pause", action="store_true", help=argparse.SUPPRESS)
args = parser.parse_args()
try:
if args.enable:
result = run_action("enable")
elif args.disable:
result = run_action("disable")
elif args.status:
result = run_action("status")
else:
return interactive()
if args.pause:
input("Press Enter to close...")
return result
except KeyboardInterrupt:
print("\nCancelled.")
return 130
except Exception as error:
print(f"Error: {error}")
return 1
if __name__ == "__main__":
raise SystemExit(main())
Step 2: Add your domains
Edit the
BLOCKED_DOMAINS list. Enter domain names only, without
https://, paths or wildcards:
Code:
BLOCKED_DOMAINS = [
"ads.example.com",
"tracking.example.com",
]
The program maps each domain to
0.0.0.0. Windows will then stop resolving that exact hostname. A hosts file does not support entries such as
*.example.com, so every required subdomain must be listed separately.
Step 3: Run the program
Open PowerShell in the folder containing the script and run:
Code:
python .\spotlock_console.py
You will see:
Code:
SpotLock Console
Current status: disabled
[Y] Enable
[N] Disable
[Q] Quit
Press
Y or
N. Windows will display its normal UAC confirmation because writing to the system hosts file requires administrator rights.
You can also use command-line arguments:
Code:
python .\spotlock_console.py --enable
python .\spotlock_console.py --disable
python .\spotlock_console.py --status
How it works
The Windows hosts file is located here:
Code:
C:\Windows\System32\drivers\etc\hosts
When enabled, the script adds a section like this:
Code:
# Begin SpoTLock AdBlock
0.0.0.0 ads.example.com
0.0.0.0 tracking.example.com
# End SpoTLock AdBlock
The start and end markers are important. When you disable the blocker, the script removes only the text between those markers. Existing entries created by Windows or other software are preserved.
Optional: Build a standalone EXE
Install PyInstaller:
Code:
py -m pip install pyinstaller
Build the executable:
Code:
py -m PyInstaller --onefile --console --name SpotLock-Console .\spotlock_console.py
The finished file will be located at:
Code:
.\dist\SpotLock-Console.exe
The Python source file is not required next to the compiled EXE. PyInstaller packages the Python runtime and your script into the executable, which is why the EXE is larger than the original script.
Note about antivirus warnings: Unsigned one-file executables can sometimes receive heuristic warnings, especially when they request administrator rights and modify a system file. Do not add Defender exclusions or attempt to bypass security software. Keep the source public, avoid packers/obfuscators, scan the release, digitally sign it if you distribute it, and submit a false-positive report to Microsoft if necessary.
Troubleshooting
Python is not recognized
Reinstall Python and enable
Add Python to PATH, or try the Windows launcher:
Code:
py .\spotlock_console.py
Permission denied
Accept the Windows UAC prompt. If it does not appear, start PowerShell as administrator and run the script again.
A website or application stopped working
Run the program and press
N, or execute:
Code:
python .\spotlock_console.py --disable
Then restart the affected application. The script already runs
ipconfig /flushdns after changing the hosts file.
The blocker does not affect a domain
Make sure you entered the exact hostname. For example,
example.com and
ads.example.com are separate hosts entries.
Final notes
This is a small example of using Python for Windows automation, privilege checks, safe text-file updates and command-line controls. Test your domain list carefully and always keep the disable function available before distributing the program.
Feedback and improvements are welcome.
Virustotal: