How to Create a Simple Hosts-Based Ad Blocker in Python
Windows console application with Enable/Disable controls
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:
Code:
python --version
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())
Edit the BLOCKED_DOMAINS list. Enter domain names only, without https://, paths or wildcards:
Code:
BLOCKED_DOMAINS = [
"ads.example.com",
"tracking.example.com",
]
Step 3: Run the program
Open PowerShell in the folder containing the script and run:
Code:
python .\spotlock_console.py
Code:
SpotLock Console Current status: disabled [Y] Enable [N] Disable [Q] Quit
You can also use command-line arguments:
Code:
python .\spotlock_console.py --enable python .\spotlock_console.py --disable python .\spotlock_console.py --status
The Windows hosts file is located here:
Code:
C:\Windows\System32\drivers\etc\hosts
Code:
# Begin SpoTLock AdBlock 0.0.0.0 ads.example.com 0.0.0.0 tracking.example.com # End SpoTLock AdBlock
Optional: Build a standalone EXE
Install PyInstaller:
Code:
py -m pip install pyinstaller
Code:
py -m PyInstaller --onefile --console --name SpotLock-Console .\spotlock_console.py
Code:
.\dist\SpotLock-Console.exe
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
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
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: [Only registered and activated users can see links. Click Here To Register...]