🐍 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

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
}
  1. Check valid
  2. Store normalized as canonical form (colon-separated uppercase)
  3. Reject is_multicast and is_broadcast for device registration
  4. 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

TypeExampleSuitable for device ID?
unicast + globally unique00:1B:44:11:3A:B7✓ Yes — manufacturer-assigned
unicast + locally administered02:xx:xx:xx:xx:xx⚠️ Warn — may be randomised by OS
multicast01:xx:xx:xx:xx:xx✗ No — group address, not a device
broadcastFF: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

See also

Ready to integrate?

Free tier — 1,000 requests/month. No credit card required.

Get your API key →