Chapter 9: Practical Examples — Reading, Writing & Debugging

This chapter brings together the concepts from all previous chapters in the form of complete, working Python examples. Each example targets a specific task: scanning an unknown tag, reading MIFARE Classic blocks, reading and writing NDEF, and debugging common errors.

All examples assume a PN532-based reader connected via USB or I2C, or an ACR122U. Hardware setup is covered in Chapter 7; library installation in Chapter 8.

Longer scripts are in the code/ folder (referenced by filename). Short illustrative snippets are shown inline.


9.1 Example 1: Scan and Identify a Tag

Goal: Detect any tag, print its type, UID, ATQA, and SAK.

Full script: code/01_scan_tag.py

import nfc

def on_connect(tag):
    print(f"Tag type : {tag.type}")
    print(f"UID      : {tag.identifier.hex().upper()}")
    if hasattr(tag, 'atq'):
        print(f"ATQA     : {tag.atq.hex().upper()}")
    if hasattr(tag, 'sak'):
        print(f"SAK      : {tag.sak:02X}")
    return False  # release tag immediately

with nfc.ContactlessFrontend('usb') as clf:
    print("Hold a tag near the reader...")
    clf.connect(rdwr={'on-connect': on_connect})

Sample output for an NTAG213:

Tag type : Type2Tag
UID      : 04 6A B3 32 A5 63 80
ATQA     : 0044
SAK      : 00

Sample output for a MIFARE Classic 1K:

Tag type : MifareClassic
UID      : A3 1F 9C 44
ATQA     : 0004
SAK      : 08

9.2 Example 2: Read a MIFARE Classic Block

Goal: Authenticate to sector 0 with Key A and read block 1.

Full script: code/02_mifare_classic_read.py

import nfc

KEY_A = bytearray.fromhex('FFFFFFFFFFFF')  # factory default

def on_connect(tag):
    if tag.type != 'MifareClassic':
        print("Not a MIFARE Classic tag")
        return False
    sector = 0
    block  = 1
    try:
        tag.authenticate(block, KEY_A, b'\x60')   # 0x60 = key A
        data = tag.read(block)
        print(f"Block {block}: {data.hex()}")
    except nfc.tag.TagCommandError as e:
        print(f"Error: {e}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Notes:


9.3 Example 3: Dump All Sectors of a MIFARE Classic 1K

Goal: Try a list of common keys on each sector and dump readable sectors.

Full script: code/03_mifare_classic_dump.py

import nfc

COMMON_KEYS = [
    bytes.fromhex('FFFFFFFFFFFF'),
    bytes.fromhex('A0A1A2A3A4A5'),
    bytes.fromhex('D3F7D3F7D3F7'),
    bytes.fromhex('000000000000'),
    bytes.fromhex('B0B1B2B3B4B5'),
]

def try_read_sector(tag, sector, keys):
    first_block = sector * 4
    for key in keys:
        for key_type, code in [(bytearray(key), b'\x60'),
                                (bytearray(key), b'\x61')]:
            try:
                tag.authenticate(first_block, key_type, code)
                blocks = [tag.read(first_block + i).hex()
                          for i in range(4)]
                return blocks, key.hex(), 'A' if code == b'\x60' else 'B'
            except Exception:
                continue
    return None, None, None

def on_connect(tag):
    if tag.type != 'MifareClassic':
        return False
    print(f"UID: {tag.identifier.hex()}")
    for sector in range(16):
        blocks, key, key_id = try_read_sector(tag, sector, COMMON_KEYS)
        if blocks:
            print(f"Sector {sector:2d} (Key {key_id}={key}):")
            for i, b in enumerate(blocks):
                print(f"  Block {sector*4+i}: {b}")
        else:
            print(f"Sector {sector:2d}: LOCKED (no matching key found)")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

9.4 Example 4: Read NDEF from an NTAG213

Goal: Read and print the NDEF message from an NTAG213 sticker.

import nfc

def on_connect(tag):
    if not tag.ndef:
        print("No NDEF data found (tag may not be NDEF-formatted)")
        return False
    print(f"NDEF version  : {tag.ndef.version}")
    print(f"NDEF capacity : {tag.ndef.capacity} bytes")
    print(f"NDEF length   : {tag.ndef.length} bytes")
    print(f"Writeable     : {tag.ndef.is_writeable}")
    print()
    for i, record in enumerate(tag.ndef.records):
        print(f"Record {i}: {record}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Sample output for an NTAG213 with a URL:

NDEF version  : 1.0
NDEF capacity : 137 bytes
NDEF length   : 15 bytes
Writeable     : True

Record 0: UriRecord('https://example.com')

9.5 Example 5: Write a URI NDEF Record to an NTAG213

Goal: Write https://example.com to a blank or already-formatted NTAG213.

import nfc, ndef

TARGET_URL = "https://example.com"

def on_connect(tag):
    if tag.ndef is None:
        print("Tag is not NDEF-formatted. Attempting format...")
        tag.format()
    records = [ndef.UriRecord(TARGET_URL)]
    tag.ndef.records = records
    print(f"Wrote NDEF URI: {TARGET_URL}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Notes:

Full script with error handling: code/05_write_ndef_uri.py


9.6 Example 6: Write NDEF to MIFARE Classic (MAD)

Writing NDEF to MIFARE Classic requires MAD setup. This is more involved: you need to write the MAD sector (sector 0) with the correct AID, then write NDEF TLV content in the NDEF sectors.

nfcpy handles this through tag.format() which writes MAD1 or MAD2 as appropriate:

import nfc, ndef

def on_connect(tag):
    if tag.type != 'MifareClassic':
        return False
    mad_key = bytearray.fromhex('A0A1A2A3A4A5')
    ndef_key = bytearray.fromhex('D3F7D3F7D3F7')
    # Format creates MAD with default keys
    tag.format(wipe=0x00)
    records = [ndef.UriRecord("https://example.com")]
    tag.ndef.records = records
    print("NDEF written to MIFARE Classic via MAD")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Note: tag.format() on MIFARE Classic writes the NDEF key (D3F7D3F7D3F7) to Key A of all NDEF sectors. This is the standard NFC Forum key for MIFARE Classic NDEF. The MAD key (A0A1A2A3A4A5) is written to sector 0 Key A. After formatting, the card is only accessible with these standard keys.


9.7 Example 7: Read Raw Pages from NTAG213

Goal: Read all user pages directly (bypassing NDEF), useful for debugging.

Full script: code/07_ntag_dump.py

import nfc

def on_connect(tag):
    if tag.type != 'Type2Tag':
        return False
    print(f"UID: {tag.identifier.hex()}")
    # Type2Tag exposes read(page) returning 16 bytes (4 pages)
    for page in range(0, 45, 4):
        data = tag.read(page)
        for i in range(4):
            offset = i * 4
            row = page + i
            print(f"  Page {row:3d}: {data[offset:offset+4].hex()}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Sample output (pages 0–7):

UID: 046ab332a56380
  Page   0: 04 6a b3 cd
  Page   1: 32 a5 63 80
  Page   2: a8 48 00 00
  Page   3: e1 10 12 00    ← Capability Container
  Page   4: 03 0f d1 01    ← NDEF TLV start
  Page   5: 0b 55 04 65
  Page   6: 78 61 6d 70
  Page   7: 6c 65 2e 63

9.8 Example 8: ACR122U with pyscard — Get UID

Goal: Use pyscard (PC/SC) to get the UID of any tag.

from smartcard.System import readers
from smartcard.util import toHexString, toBytes

r = readers()
if not r:
    print("No readers found")
    exit()

print(f"Using reader: {r[0]}")
conn = r[0].createConnection()
conn.connect()

# ACR122U GET UID pseudo-APDU
get_uid = [0xFF, 0xCA, 0x00, 0x00, 0x00]
data, sw1, sw2 = conn.transmit(get_uid)
if sw1 == 0x90:
    print(f"UID: {toHexString(data)}")
else:
    print(f"Error: SW={sw1:02X}{sw2:02X}")
conn.disconnect()

9.9 Debugging Common Errors

NAK (Negative Acknowledgement)

Symptoms: TagCommandError: [NAK], authentication failure, write failure.

Causes and fixes:

NFCID / Anticollision errors

Symptoms: Tag detected but immediately lost; inconsistent reads.

Causes and fixes:

“No such device” / reader not found

nfc.clf.device.Error: [Errno 13] Permission denied

Fix (Linux): Add udev rule or add your user to the plugdev group:

sudo usermod -aG plugdev $USER
# then log out and back in

pcscd / libnfc conflict

If both pcscd and a libnfc-based tool try to claim the same USB reader:

sudo service pcscd stop   # temporarily stop PC/SC daemon
# ... run libnfc tool ...
sudo service pcscd start  # restart

Or permanently blacklist the ACR122U from pcscd by adding it to /etc/modprobe.d/blacklist.conf:

blacklist pn533
blacklist nfc

NDEF not found on a formatted tag

Symptoms: tag.ndef is None despite the tag being “NFC-enabled”.

Causes:

Write fails silently

Check:


9.10 Example 9: Encode and Parse NDEF Manually with ndeflib

Goal: Build an NDEF message by hand, encode to bytes, parse back.

import ndef

# Build a multi-record Smart Poster: URL + title
records = [
    ndef.SmartposterRecord(
        resource="https://example.com",
        title=[("en", "Example website"), ("fr", "Site exemple")],
        action="open",
    )
]
encoded = b"".join(ndef.message_encoder(records))
print(f"Encoded ({len(encoded)} bytes): {encoded.hex()}")

# Parse back
for record in ndef.message_decoder(encoded):
    print(f"Type: {type(record).__name__}")
    print(f"  URI: {record.resource}")
    for lang, text in record.title:
        print(f"  Title [{lang}]: {text}")

Summary


← Chapter 8: Software Libraries Table of Contents Chapter 10: Action Plan →