🐍 PythonNetworking / IoT
MAC Address Validation in Python
Validate MAC (Media Access Control) addresses in Python — accept colon, hyphen, or compact formats, detect address types (unicast, multicast, broadcast), identify locally-administered (randomised) MACs, and normalise to a canonical form for storage.
Also available in Node.js · For IMEI + MAC together see the IoT Device Validation guide
Contents
1. MAC address structure
A MAC address is a 48-bit (6-byte) identifier. The first 3 bytes form the OUI (Organizationally Unique Identifier) assigned to the manufacturer; the last 3 bytes are the device serial number.
00 : 1B : 44 : 11 : 3A : B7 ├───────────┤ ├───────────┤ OUI (3 bytes) NIC-specific (3 bytes) Manufacturer Device serial Bit 0 of byte 0: 0 = unicast, 1 = multicast Bit 1 of byte 0: 0 = globally unique (OUI-assigned) 1 = locally administered (may be randomised)
2. API response structure
Endpoint: GET /v0/mac-address?value=…
{ "valid": true, "normalized": "00:1B:44:11:3A:B7", "format": "colon", "type": "unicast", "isMulticast": false, "isLocal": false, "isBroadcast": false }
- Check
valid - Store
normalizedas canonical form (colon-separated uppercase) - Reject
is_multicastandis_broadcastfor device registration - Warn on
is_local— likely a privacy/randomised MAC
3. Accepted input formats
# All three formats are accepted — normalized is always colon-separated uppercase await iv.mac_address("00:1B:44:11:3A:B7") # colon await iv.mac_address("00-1B-44-11-3A-B7") # hyphen await iv.mac_address("001B44113AB7") # compact await iv.mac_address("00:1b:44:11:3a:b7") # lowercase — also accepted # All return normalized: "00:1B:44:11:3A:B7"
4. Address types
| Type | Example | Suitable for device ID? |
|---|---|---|
| unicast + globally unique | 00:1B:44:11:3A:B7 | ✓ Yes — manufacturer-assigned |
| unicast + locally administered | 02:xx:xx:xx:xx:xx | ⚠️ Warn — may be randomised by OS |
| multicast | 01:xx:xx:xx:xx:xx | ✗ No — group address, not a device |
| broadcast | FF:FF:FF:FF:FF:FF | ✗ No — reserved broadcast address |
5. Locally-administered (randomised) addresses
⚠️iOS 14+, Android 10+, and Windows 10+ randomise Wi-Fi MAC addresses per network by default. If
is_local: True, the address may change when the user reconnects to a different network or resets their device. Do not use as a stable device identifier without an explicit user opt-out of MAC randomisation.result = await iv.mac_address(mac) if result.valid and result.is_local: # Log a warning — the user's device may be using MAC randomisation logger.warning( "Locally-administered MAC %s — may be randomised", result.normalized, ) # Option: require the user to disable MAC randomisation for this network # and resubmit, or fall back to a different device identifier (IMEI, UUID)
6. Validation with asyncio.gather
import asyncio import logging import os from isvalid_sdk import IsValidConfig, create_client logger = logging.getLogger(__name__) iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) async def validate_mac(mac: str) -> dict: result = await iv.mac_address(mac) if not result.valid: raise ValueError(f"Invalid MAC address: {mac!r}") if result.is_broadcast: raise ValueError("Broadcast address — not a valid device identifier") if result.is_multicast: raise ValueError("Multicast address — not suitable for device registration") if result.is_local: logger.warning("MAC %s is locally-administered — may be randomised", result.normalized) return result # Register device — validate MAC and normalise before DB insert async def register_device(raw_mac: str, device_name: str) -> dict: mac = await validate_mac(raw_mac) return {"mac": mac.normalized, "type": mac.type, "device_name": device_name} # Batch validate from network scan async def validate_network_scan(macs: list[str]) -> list[dict]: tasks = [iv.mac_address(m) for m in macs] results = await asyncio.gather(*tasks, return_exceptions=True) return [ { "mac": mac, "result": r if not isinstance(r, Exception) else None, "error": str(r) if isinstance(r, Exception) else None, } for mac, r in zip(macs, results) ] async def main(): device = await register_device("00-1B-44-11-3A-B7", "Raspberry Pi sensor") print(device["mac"]) # "00:1B:44:11:3A:B7" asyncio.run(main())
7. Summary checklist
✓Accept colon, hyphen, and compact formats
✓Store normalized (colon uppercase) as canonical form
✓Reject multicast and broadcast addresses
✓Warn on locally-administered (randomised) MACs
✓Do not use MAC as sole persistent ID (may change)
✓Consider IMEI or UUID as stable device ID fallback
✓Run batch scans with asyncio.gather
✓Return 422 with reason on invalid MAC input