If you're writing Python code, macos-messages gives you a clean API to work with your Messages.app data. This page covers everything available in the library.

Getting started

The simplest way to get going is with get_db():

import messages

db = messages.get_db()

# Now you can query your messages
for chat in db.chats(limit=5):
    print(f"{chat.display_name}: {chat.message_count} messages")

get_db()

Creates a database connection. This is the main entry point you'll use.

messages.get_db(path: str | None = None) -> MessagesDB
Parameter Type What it does
path str \| None Path to chat.db. If you don't provide one, it defaults to ~/Library/Messages/chat.db

Returns: A MessagesDB instance

Raises:

  • FileNotFoundError if the database doesn't exist
  • PermissionError if Full Disk Access hasn't been granted

Example:

# Use the default location
db = messages.get_db()

# Or point to a specific database file (like a backup)
db = messages.get_db("/path/to/backup/chat.db")

MessagesDB

This is the main class you'll work with. It provides methods to query chats, messages, and attachments.

Creating an instance

You can use get_db() (shown above) or create one directly:

from messages.db import MessagesDB

db = MessagesDB(path="/path/to/chat.db", resolve_contacts=True)
Parameter Type What it does
path Path \| str \| None Path to chat.db
resolve_contacts bool Whether to look up contact names from Contacts.app (default: True)

chats()

Lists your conversations, ordered by most recent activity.

db.chats(
    *,
    service: str | None = None,
    limit: int | None = None,
) -> Iterator[ChatSummary]
Parameter Type What it does
service str \| None Filter by service: "iMessage", "SMS", or "RCS"
limit int \| None Maximum number of results

Example:

# Get your 10 most recent conversations
for chat in db.chats(limit=10):
    print(f"{chat.id}: {chat.display_name or chat.identifier}")

# Only iMessage conversations
for chat in db.chats(service="iMessage"):
    print(chat.display_name)

chat()

Gets a single chat by its ID.

db.chat(chat_id: int) -> Chat

chat_by_identifier()

Gets a chat by phone number or email. The nice thing here is that phone number matching is smart - you can use any format and it'll find the right conversation.

db.chat_by_identifier(identifier: str) -> Chat

Example:

# All of these will find the same conversation
chat = db.chat_by_identifier("+1 555 123 4567")
chat = db.chat_by_identifier("555-123-4567")
chat = db.chat_by_identifier("5551234567")
chat = db.chat_by_identifier("jane@example.com")

messages()

Lists messages in chronological order (oldest first).

db.messages(
    *,
    chat_id: int | None = None,
    identifier: str | None = None,
    after: datetime | None = None,
    before: datetime | None = None,
    limit: int = 100,
    offset: int = 0,
    include_unsent: bool = True,
    unread: bool = False,
    include_unknown_senders: bool = False,
) -> Iterator[Message]
Parameter Type What it does
chat_id int \| None Filter by chat ID
identifier str \| None Filter by phone number or email (use this instead of chat_id if you prefer)
after datetime \| None Only include messages after this date
before datetime \| None Only include messages before this date
limit int Maximum number of results (default: 100)
offset int Skip the first N results
include_unsent bool Include messages that were unsent (default: True)
unread bool Only return unread incoming messages (default: False)
include_unknown_senders bool Include messages from unknown/filtered senders (default: False, excludes spam)

Example:

from datetime import datetime

# Get messages by chat ID
for msg in db.messages(chat_id=42, limit=20):
    print(f"{msg.date}: {msg.text}")

# Get messages by phone number
for msg in db.messages(identifier="+15551234567"):
    print(msg.text)

# Get messages from the last month
for msg in db.messages(chat_id=42, after=datetime(2024, 1, 1)):
    print(msg.text)

# Get all unread messages (excludes spam by default)
for msg in db.messages(unread=True):
    print(f"Unread from {msg.sender.display_name}: {msg.text}")

# Get all unread messages including spam
for msg in db.messages(unread=True, include_unknown_senders=True):
    print(msg.text)

message()

Gets a single message by its ID, with all the details.

db.message(message_id: int) -> Message

Searches messages by text content. The search is case-insensitive.

db.search(
    query: str,
    *,
    chat_id: int | None = None,
    chat_ids: list[int] | None = None,
    after: datetime | None = None,
    before: datetime | None = None,
    limit: int = 50,
    unread: bool = False,
    include_unknown_senders: bool = False,
) -> Iterator[Message]
Parameter Type What it does
query str The text to search for
chat_id int \| None Only search within a specific chat
chat_ids list[int] \| None Search within multiple chats (merged results)
after datetime \| None Only search messages after this date
before datetime \| None Only search messages before this date
limit int Maximum number of results (default: 50)
unread bool Only return unread incoming messages (default: False)
include_unknown_senders bool Include messages from unknown/filtered senders (default: False, excludes spam)

Example:

for msg in db.search("dinner tomorrow"):
    print(f"[Chat {msg.chat_id}] {msg.text}")

# Search unread messages (excludes unknown senders by default)
for msg in db.search("lunch", unread=True):
    print(msg.text)

attachments()

Lists file attachments.

db.attachments(
    *,
    chat_id: int | None = None,
    message_id: int | None = None,
    mime_type: str | None = None,
    limit: int = 100,
    auto_download: bool = True,
) -> Iterator[Attachment]
Parameter Type What it does
chat_id int \| None Filter by chat
message_id int \| None Filter by message
mime_type str \| None Filter by MIME type (e.g., "image/*")
limit int Maximum number of results (default: 100)
auto_download bool Automatically download iCloud attachments (default: True)

Example:

# Get all images from a chat
for att in db.attachments(chat_id=42, mime_type="image/*"):
    print(f"{att.filename}: {att.path}")

download_attachment()

Makes sure an attachment is downloaded locally. For attachments stored in iCloud, this triggers the download and waits for it to complete.

db.download_attachment(attachment: Attachment) -> Path

Models

These are the data classes you'll get back from queries.

Message

A single message.

@dataclass
class Message:
    id: int
    chat_id: int
    text: str | None
    date: datetime
    is_from_me: bool
    sender: Handle | None
    has_attachments: bool
    reactions: list[Reaction]
    effect: MessageEffect | None
    edit_history: list[EditRecord]
    is_edited: bool
    is_unsent: bool
    transcription: str | None  # For audio messages
    reply_to_id: int | None    # For threaded replies
    thread_id: int | None

Example:

msg = db.message(12345)

print(f"From: {'me' if msg.is_from_me else msg.sender.display_name}")
print(f"Text: {msg.text}")

if msg.reactions:
    for r in msg.reactions:
        print(f"  {r.sender.display_name} reacted with {r.type.value}")

if msg.is_edited:
    print(f"  Edited {len(msg.edit_history)} times")

Chat

A conversation with full details.

@dataclass
class Chat:
    id: int
    identifier: str
    display_name: str | None
    service: str
    participants: list[Handle]

ChatSummary

A lightweight version of Chat, returned by chats(). Includes message counts.

@dataclass
class ChatSummary:
    id: int
    identifier: str
    display_name: str | None
    service: str
    message_count: int
    last_message_date: datetime | None

Handle

A contact identifier - either a phone number or email address.

@dataclass
class Handle:
    id: int
    identifier: str       # The phone number or email
    service: str          # "iMessage", "SMS", or "RCS"
    display_name: str | None  # Resolved from Contacts.app

Attachment

A file attachment.

@dataclass
class Attachment:
    id: int
    message_id: int
    filename: str
    mime_type: str | None
    path: str            # Local path (might not exist if it's in iCloud)
    size: int
    is_sticker: bool

Reaction

A tapback reaction on a message.

@dataclass
class Reaction:
    type: ReactionType
    sender: Handle
    date: datetime

ReactionType

The different types of tapback reactions.

class ReactionType(Enum):
    LOVE = "love"           # ❤️
    LIKE = "like"           # 👍
    DISLIKE = "dislike"     # 👎
    LAUGH = "ha-ha"         # 😂
    EMPHASIS = "emphasis"   # ‼️
    QUESTION = "question"   # ❓

MessageEffect

iMessage bubble and screen effects.

class MessageEffect(Enum):
    # Bubble effects
    SLAM = "slam"
    LOUD = "loud"
    GENTLE = "gentle"
    INVISIBLE_INK = "invisible_ink"

    # Screen effects
    ECHO = "echo"
    SPOTLIGHT = "spotlight"
    BALLOONS = "balloons"
    CONFETTI = "confetti"
    LOVE_EFFECT = "love_effect"
    LASERS = "lasers"
    FIREWORKS = "fireworks"
    CELEBRATION = "celebration"

EditRecord

A single edit in a message's history.

@dataclass
class EditRecord:
    text: str
    date: datetime

Putting it all together

Here's a more complete example that shows how these pieces fit together:

import messages
from datetime import datetime, timedelta

db = messages.get_db()

# Find conversations with "Mom" in the name
mom_chats = [
    chat for chat in db.chats()
    if chat.display_name and "Mom" in chat.display_name
]

if mom_chats:
    mom = mom_chats[0]
    print(f"Found: {mom.display_name} ({mom.message_count} messages)")

    # Get messages from the last 7 days
    week_ago = datetime.now() - timedelta(days=7)

    for msg in db.messages(chat_id=mom.id, after=week_ago):
        sender = "Me" if msg.is_from_me else mom.display_name
        print(f"[{msg.date:%b %d %H:%M}] {sender}: {msg.text}")

        # Show reactions
        for r in msg.reactions:
            print(f"    {r.type.value} from {r.sender.display_name}")

    # Find all photos shared in this conversation
    photos = list(db.attachments(chat_id=mom.id, mime_type="image/*"))
    print(f"\n{len(photos)} photos shared")