How to Read a Smart Card with Python (pyscard)

This tutorial walks you through everything you need to read smart cards from Python: installing pyscard, listing readers, connecting to cards, sending APDU commands, parsing ATR, and monitoring card events. Works on Windows, macOS, and Linux with any PC/SC reader (ACR122U, HID Omnikey, Identiv, etc.).

1. Install pyscard and pcsc-lite

Windows

pip install pyscard

No additional setup needed — Windows has the PC/SC subsystem built-in (winscard.dll).

Linux (Ubuntu/Debian)

sudo apt update
sudo apt install pcscd pcsc-tools libpcsclite-dev
pip install pyscard
sudo systemctl start pcscd

macOS

pip install pyscard  # PC/SC framework built-in on macOS
Verify installation: python -c "import smartcard; print('OK')" should print OK without errors.

2. List Connected Smart Card Readers

from smartcard.System import readers

# Get all connected readers
r = readers()
if not r:
    print("No readers found. Is your reader plugged in?")
    exit(1)

for i, reader in enumerate(r):
    print(f"Reader {i}: {reader}")

Output example:

Reader 0: ACS ACR122U PICC Interface 00 00
Reader 1: HID OMNIKEY 5022 Smart Card Reader 0

3. Connect to a Card and Get ATR

from smartcard.System import readers

r = readers()
connection = r[0].createConnection()
connection.connect()

# ATR is available after connect()
print(f"ATR: {connection.getATR():02X}")
print(f"Protocol: {connection.getProtocol()}")
# ATRIBUTE_ATR_STRING = 0x00DE is not needed — getATR() does it

Parse the ATR

The ATR (Answer to Reset) tells you the card's protocol, data rate, and historical bytes. You can decode ATR hex manually or use our ATR Decoder Tool for a full breakdown.

# Quick ATR parsing
atr = connection.getATR()
ts = atr[0]  # TS byte: 0x3B = direct convention, 0x23 = inverse
t0 = atr[1]  # T0: Y1(high nibble) + K(low nibble)
print(f"Convention: {'Direct' if ts == 0x3B else 'Inverse'}")
print(f"Historical bytes: {t0 & 0x0F}")

4. Send Your First APDU — SELECT

To read data from a card, you first select its application (DF) or root directory (MF).

from smartcard.util import toHexString

# SELECT MF (Master File, 3F00) — works on most cards
SELECT_MF = [0x00, 0xA4, 0x00, 0x00, 0x02, 0x3F, 0x00]
response, sw1, sw2 = connection.transmit(SELECT_MF)

print(f"Response data: {toHexString(response)}")
print(f"Status Word: {sw1:02X} {sw2:02X}")

if sw1 == 0x90 and sw2 == 0x00:
    print("MF selected — card is ready")
elif sw1 == 0x61:
    # T=0 card: use GET RESPONSE to fetch remaining data
    print(f"T=0: {sw2} bytes available, send GET RESPONSE")
else:
    print(f"Error: SW1={sw1:02X} SW2={sw2:02X}")

5. Read a File — READ BINARY Example

# Read ICCID from a SIM card (EF at 0x2FE2)
SELECT_ICCID = [0x00, 0xA4, 0x00, 0x00, 0x02, 0x2F, 0xE2]
_, sw1, sw2 = connection.transmit(SELECT_ICCID)

if sw1 == 0x90 and sw2 == 0x00:
    # File selected — read 10 bytes
    READ_BINARY = [0x00, 0xB0, 0x00, 0x00, 0x0A]
    data, sw1, sw2 = connection.transmit(READ_BINARY)
    if sw1 == 0x90:
        print(f"ICCID: {toHexString(data)}")
    elif sw1 == 0x61:
        # T=0: data available via GET RESPONSE
        GET_RESPONSE = [0x00, 0xC0, 0x00, 0x00, sw2]
        data, sw1, sw2 = connection.transmit(GET_RESPONSE)
        print(f"ICCID: {toHexString(data)}")

6. Handle T=0 GET RESPONSE Properly

The most common gotcha. When a T=0 card responds with 61 XX, you must send a separate GET RESPONSE to retrieve the data.

def transmit_apdu(connection, apdu):
    """Send APDU and handle T=0 GET RESPONSE automatically."""
    resp, sw1, sw2 = connection.transmit(apdu)
    if sw1 == 0x61:
        # GET RESPONSE with Le from SW2
        get_resp = [0x00, 0xC0, 0x00, 0x00, sw2]
        resp, sw1, sw2 = connection.transmit(get_resp)
    elif sw1 == 0x6C:
        # Wrong Le — retry with correct Le from SW2
        apdu[-1] = sw2
        resp, sw1, sw2 = connection.transmit(apdu)
    return resp, sw1, sw2

# Usage:
response, sw1, sw2 = transmit_apdu(connection, SELECT_MF)
Pro tip: Our APDU Response Debugger decodes SW1 SW2 in real time — paste your response hex and get instant explanations.

7. Monitor Card Insertion and Removal

from smartcard.CardMonitoring import CardMonitor, CardObserver
import time

class CardWatcher(CardObserver):
    def update(self, observable, actions):
        (added, removed) = actions
        for card in added:
            print(f"Card INSERTED: {card.atr}")
        for card in removed:
            print("Card REMOVED")

monitor = CardMonitor()
watcher = CardWatcher()
monitor.addObserver(watcher)

print("Waiting for card events... (Ctrl+C to stop)")
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    monitor.deleteObserver(watcher)
    print("Done")

8. Send Commands to a Specific Protocol (T=0 or T=1)

# Force T=0
conn_t0 = r[0].createConnection()
conn_t0.connect(protocol="T=0")

# Force T=1
conn_t1 = r[0].createConnection()
conn_t1.connect(protocol="T=1")

# Auto-negotiate (recommended)
conn_auto = r[0].createConnection()
conn_auto.connect()  # Negotiates highest available protocol

Common Error Codes and Fixes

ErrorMeaningFix
No readers foundReader not detectedCheck USB, reinstall driver, restart pcscd (Linux)
Card not presentNo card on readerInsert card before connecting
Connection refusedAnother app locked the readerClose other smart card apps, restart pcscd
SmartcardException: transmit()APDU command failedCheck CLA/INS/P1/P2 with APDU Builder

Try Our Tools While Coding

APDU Command Builder — Build commands visually and copy hex to your Python code | APDU Response Debugger — Debug SW1 SW2 | ATR Decoder — Parse ATR hex | PC/SC & CCID Programming Guide — Deep dive into reader architecture