Export YouTube Comments to CSV With Python and yt-dlp

Export a bounded public YouTube comment sample with Python and yt-dlp, optional proxy routing, UTF-8 CSV output, and formula-injection protection.

Written by the Mexela Editorial Team. Technical guides are reviewed by the Mexela Technical Team under the Mexela Editorial Policy.

Red and white Python terminal beside public video comment rows and a proxy server

PROXY PLANS

Ready to buy proxies for this workflow?

Use the guide below to choose the right proxy type, then start with private proxies for dedicated IPv4 access or shared proxies when price matters more.

For a one-video, bounded public comment export, Python can ask yt-dlp for comment metadata without downloading media, limit the returned sample, neutralize spreadsheet formulas, and write explicit UTF-8 CSV. Use an optional proxy only when the approved network route requires one. For a maintained application that needs documented thread IDs, paging, ordering, and API errors, prefer the official YouTube Data API commentThreads.list method.

The program below exports at most 200 comments from one VIDEO_URL. It writes six fields and protects every cell whose first character is =, +, -, or @. It does not download video or audio, recover deleted comments, or promise that yt-dlp can extract comments from every public video.

Choose the comment source from the required output

The official commentThreads.list reference returns structured thread resources, costs 1 quota unit per call, accepts maxResults from 1 to 100, and supplies nextPageToken when another page exists. It is the better choice when a product needs stable API semantics, controlled ordering, or explicit error details.

A thread response can include a top-level comment and a subset of replies; it is not automatically every reply. The API reference also documents a 403 commentsDisabled error for a video with comments disabled. Treat that as an availability state, not an invitation to switch routes until something returns.

yt-dlp is useful for a small reviewed metadata job where the extractor already supports the public page. The official yt-dlp README documents --write-comments/--get-comments, the YouTube max_comments extractor argument, and HTTP, HTTPS, or SOCKS proxy URLs. Extractor behavior can change when YouTube changes, so pin the installed version in the run log.

Install the tool and declare the boundaries

Create a virtual environment, then install a reviewed release with python -m pip install yt-dlp. Set VIDEO_URL to one approved public watch URL. Set PROXY_URL only when the project requires that route; otherwise leave it unset. A proxy URL containing credentials belongs in an environment variable or secret manager, not the program or CSV.

The script keeps COMMENT_LIMIT = 200 in source so reviewers can see the ceiling. It passes that value through the YouTube max_comments extractor argument and slices the returned list again before writing. The second limit is intentional defensive behavior if extractor semantics change or the returned object contains more rows than requested.

Run the complete Python exporter

import csv
import os
import sys
from datetime import datetime, timezone
from yt_dlp import YoutubeDL
VIDEO_URL = os.environ.get("VIDEO_URL", "").strip()
PROXY_URL = os.environ.get("PROXY_URL", "").strip()
COMMENT_LIMIT = 200
OUTPUT_PATH = "youtube_comments.csv"
FIELDNAMES = [
    "video_id",
    "comment_id",
    "parent_id",
    "text",
    "published_at",
    "captured_at",
]
def safe_cell(value):
    text = "" if value is None else str(value)
    if text.startswith(("=", "+", "-", "@")):
        return "'" + text
    return text
def normalize_comment(video_id, comment, captured_at):
    timestamp = comment.get("timestamp")
    published_at = ""
    if isinstance(timestamp, (int, float)):
        published_at = datetime.fromtimestamp(
            timestamp, tz=timezone.utc
        ).isoformat()
    raw = {
        "video_id": video_id,
        "comment_id": comment.get("id"),
        "parent_id": comment.get("parent"),
        "text": comment.get("text") or "",
        "published_at": published_at,
        "captured_at": captured_at,
    }
    return {name: safe_cell(raw.get(name)) for name in FIELDNAMES}
def main():
    if not VIDEO_URL:
        raise ValueError("VIDEO_URL is required")
    if not VIDEO_URL.startswith(("https://www.youtube.com/", "https://youtu.be/")):
        raise ValueError("VIDEO_URL must be a YouTube watch URL")
    options = {
        "skip_download": True,
        "getcomments": True,
        "quiet": True,
        "extractor_args": {
            "youtube": {"max_comments": [str(COMMENT_LIMIT)]}
        },
    }
    if PROXY_URL:
        options["proxy"] = PROXY_URL
    with YoutubeDL(options) as ydl:
        info = ydl.extract_info(VIDEO_URL, download=False)
    video_id = info.get("id")
    if not video_id:
        raise RuntimeError("yt-dlp returned no video ID")
    comments = info.get("comments") or []
    if not comments:
        raise RuntimeError(
            "No comments returned; they may be disabled, unavailable, "
            "or unsupported by this extractor version"
        )
    captured_at = datetime.now(timezone.utc).isoformat()
    rows = [
        normalize_comment(video_id, comment, captured_at)
        for comment in comments[:COMMENT_LIMIT]
    ]
    with open(OUTPUT_PATH, "w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=FIELDNAMES, extrasaction="raise")
        writer.writeheader()
        writer.writerows(rows)
    print(f"Wrote {len(rows)} comments to {OUTPUT_PATH}")
if __name__ == "__main__":
    try:
        main()
    except Exception as error:
        print(
            f"Export failed ({type(error).__name__}). "
            "Check the video, comment availability, yt-dlp version, and route.",
            file=sys.stderr,
        )
        sys.exit(1)

In PowerShell, run $env:VIDEO_URL='https://www.youtube.com/watch?v=VIDEO_ID', optionally set $env:PROXY_URL='http://user:password@host:port', and execute python export-comments.py. The success message states the number of rows and output filename. The failure path deliberately avoids printing the exception text because a network error can contain the configured proxy URL.

Explicit newline="" lets Python’s CSV module manage line endings correctly, including comments containing line breaks. Explicit encoding="utf-8" preserves multilingual text. DictWriter quotes commas, quotes, and newlines where required, while extrasaction="raise" prevents an accidental new field from silently changing the export contract.

Neutralize spreadsheet formulas in every column

CSV is data, but spreadsheet applications may interpret a leading =, +, -, or @ as a formula. A public comment can deliberately begin with one of those characters. IDs can also begin with a hyphen, so protecting only the comment text is incomplete. safe_cell converts each value to text and prefixes an apostrophe when the first character is risky.

The exact header is video_id,comment_id,parent_id,text,published_at,captured_at. One illustrative sanitized row is:

video_id comment_id parent_id text published_at captured_at
demoVideo01 demoComment01 root '=HYPERLINK(“https://example.invalid”,”click”) 2026-07-15T10:00:00+00:00 2026-07-17T09:30:00+00:00

The leading apostrophe is part of the exported value; it tells common spreadsheet software to display the following characters as text. Retain the original text only in a protected raw source if the approved analysis needs byte-for-byte evidence. Do not remove the first character, because that would alter the comment.

Interpret an empty or partial export correctly

Zero rows can mean comments are disabled, the video is unavailable to the current session, consent or authentication is required, the extractor changed, or the public page returned no supported comments. The script reports a single nonzero failure rather than writing a plausible-looking empty file. Record the yt-dlp version with python -m yt_dlp --version and the capture time before investigating.

The yt-dlp limit is an upper bound, not a statement that the first 200 rows represent every audience response. Comment sorting, replies, moderation, deletion, pinned comments, and extractor behavior influence the sample. Label the file “bounded yt-dlp public sample,” state the requested cap, and never call it a complete thread archive.

If replies are essential, inspect the official API resource model. The commentThread resource documentation explains that a thread may not contain every reply and points to comments.list for retrieving all replies to a top-level comment. That explicit relationship is preferable to inferring completeness from an extractor list.

Use a proxy only for the approved route requirement

The proxy option sends yt-dlp network requests through the supplied URL. It does not enable comments, remove moderation, or grant access to private or deleted material. Verify the route before the export with the proxy check procedure, then keep the same route for the bounded job so errors are reproducible.

Do not write PROXY_URL to the CSV, console, exception message, or report. If authentication fails, separate URL encoding, credential validity, and endpoint availability using the proxy authentication guide. Repeated retries can duplicate work and add load; stop, diagnose, and rerun once the route is known.

Review the file before opening it in a spreadsheet

Check that the header is exact, the row count matches the success message, comment IDs are unique where present, and every row has the same video ID. Open a copy in a plain-text editor first. Then inspect cells beginning with apostrophes in the target spreadsheet to confirm they display as text rather than executing.

Comment text and channel-linked identifiers may be personal data. Keep only fields necessary for the stated analysis, restrict access, set a deletion date, and avoid publishing raw comments in a report. Google’s YouTube API Services developer policies remain the authority for applications using API data; yt-dlp use also requires a project-specific review of applicable terms and rights.

If the approved export needs one stable country route and a fixed address until the CSV closes, compare Mexela’s proxy plans by country and session persistence. The proxy is transport configuration, not a way to expand the comment scope.

Frequently asked questions

Why protect IDs as well as comment text?

The safety rule applies to every spreadsheet cell. An identifier or timestamp-like value can also begin with a formula-trigger character, so the exporter runs every field through the same function.

Will yt-dlp always return 200 comments?

No. Two hundred is a ceiling. The video may have fewer supported comments, comments may be disabled or unavailable, and extractor or session conditions can produce fewer rows or an explicit failure.

When should the official API replace this script?

Use commentThreads.list when you need documented pagination, ordering, resource IDs, quota behavior, explicit API errors, or a maintained production integration.

Does the proxy retrieve removed comments?

No. It changes the request route only and does not make private, deleted, moderated, or otherwise unavailable comments accessible.