{"id":41,"date":"2026-10-01T10:18:28","date_gmt":"2026-10-01T04:48:28","guid":{"rendered":"https:\/\/truepythoneer.com\/blog\/?p=41"},"modified":"2026-10-01T10:18:29","modified_gmt":"2026-10-01T04:48:29","slug":"amaze-your-friends-painting-images-google-sheets-python","status":"publish","type":"post","link":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/","title":{"rendered":"Amaze Your Friends by Painting Images on Google Sheets Using Python"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Google Sheets looks like a tool for numbers, but underneath it is a grid of cells, and every cell can be given a color. Three years ago, on 1 October 2023, I stood on the PyCon India 2023 stage in Hyderabad and showed how Python can use that grid as a canvas for painting images on Google Sheets, one cell per pixel. To mark the third anniversary of that talk, this post walks through the idea, the Google Cloud setup and the code, based on my final slides and the real repository.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before the talk, I recorded a short video for my PyCon India proposal, with the explanation and a live demo. <a href=\"https:\/\/youtu.be\/H2J7bXMvqL4\">Watch the proposal video on YouTube<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can also read the <a href=\"https:\/\/in.pycon.org\/cfp\/pycon-india-2023\/proposals\/amaze-your-friends-by-painting-images-on-google-sheets-using-python~eER80\/\">original proposal on the PyCon India 2023 site<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The idea: a spreadsheet is just a grid of pixels<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A Google Sheet is a grid of cells, and every cell can have a background color. Similarly, an image is a grid of pixels, and every pixel has a color. As a result, if you match one cell to one pixel, the spreadsheet becomes a canvas for painting images on Google Sheets.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is the whole talk, and it came from a small experiment. First, in 2020 I wrote a script that turned images into Excel files, using the xlsxwriter and openpyxl libraries. However, my Python history goes back further: in 2008 I wrote a Sudoku solver for Nokia Symbian 60 phones using Mobile Python. Then, for PyCon India 2023, I moved the idea to Google Sheets, where an API can do the painting from anywhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In practice, the problem breaks into three steps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Read the pixel data of an image.<\/li>\n\n\n\n<li>Set up a Google Spreadsheet as the canvas.<\/li>\n\n\n\n<li>Fill each cell with the color of its pixel.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Two terms matter throughout, because the API uses both. A <strong>spreadsheet<\/strong> is the whole book, and a <strong>sheet<\/strong> is one page (tab) inside it. The script adds a new sheet for every image you paint, so you can keep many pictures in one spreadsheet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setting up Google Cloud<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Painting images on Google Sheets from Python only works if Google trusts your code. So five setup steps get you there, all in the Google Cloud console.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Create a Google Cloud project.<\/strong> I named mine ImageToSheet.<\/li>\n\n\n\n<li><strong>Enable the Google Sheets API.<\/strong> Open APIs &amp; Services, choose Enable APIs and Services, search for Google Sheets API and enable it.<\/li>\n\n\n\n<li><strong>Create a service account.<\/strong> Google&#8217;s credential guide lists three options: API keys (public data), OAuth client ID (user data with consent) and service account (data owned by your app). Therefore, a script that edits its own sheet needs the service account. I called mine <code>pythonsheetclient<\/code>.<\/li>\n\n\n\n<li><strong>Create a key for the service account.<\/strong> Open the account, go to the Keys tab, then Add Key and choose JSON. Google warns that downloaded keys are a security risk if they leak, so keep the file out of shared folders. Save it as key.json next to the script; the repository&#8217;s .gitignore already excludes that name.<\/li>\n\n\n\n<li><strong>Share the spreadsheet with the service account.<\/strong> The service account has its own email address, ending in <code>iam.gserviceaccount.com<\/code>. Share the sheet with that address as an Editor, just as you would with a colleague.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">After step 5 the script can open the spreadsheet with the key file. As a result, nothing else needs to be public.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Python, file by file<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The project for painting images on Google Sheets is three small files, released under the GNU General Public License v3.0:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>File<\/th><th>Job<\/th><\/tr><\/thead><tbody><tr><td><code>ImageToGoogleSheet.py<\/code><\/td><td>The main script: loads the image, adds a sheet, sends the pixels<\/td><\/tr><tr><td><code>ImageToJSON.py<\/code><\/td><td>Turns the pixel array into one big <code>updateCells<\/code> request<\/td><\/tr><tr><td><code>RequestDataClasses.py<\/code><\/td><td>The dataclasses that describe that request<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Get the code:<\/strong> the full project, including the notebook and sample images, is on GitHub: <a href=\"https:\/\/github.com\/truepythoneer\/ImageToGoogleSheet\">https:\/\/github.com\/truepythoneer\/ImageToGoogleSheet<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The snippets below come straight from the repository, with only the progress <code>print<\/code> lines trimmed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Before you run it<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create a fresh environment (I used Anaconda and installed the packages from conda-forge), then install the libraries: <code>pip install pillow numpy google-api-python-client google-auth google-auth-httplib2 httplib2 dataclasses-json tqdm colorama<\/code>.<\/li>\n\n\n\n<li>Save the service account key next to the script as <code>key.json<\/code>. The repository&#8217;s <code>.gitignore<\/code> already excludes <code>key.json<\/code> and <code>*.log<\/code>, so the key stays out of Git.<\/li>\n\n\n\n<li>Set the environment variable <code>SPREADSHEETID<\/code> to your spreadsheet&#8217;s ID. It is the part of the URL after <code>\/d\/<\/code>, and the number after <code>gid=<\/code> is the <code>sheetId<\/code> of one tab:<\/li>\n<\/ol>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">https:\/\/docs.google.com\/spreadsheets\/d\/<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">spreadsheetId<\/span>&gt;<\/span>\/edit#gid=<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">sheetId<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Then run it with the path of any image:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">python ImageToGoogleSheet.py images\/monasmall.png<\/code><\/span><\/pre>\n\n\n<h3 class=\"wp-block-heading\">1. The class and its settings<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">First, everything hangs off one class. <code>SQUAREWIDTH<\/code> is the pixel size given to every column, and the spreadsheet ID and key file are read here.<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">class ImageToGoogleSheet:\n\n    def __init__(self):\n        self.imagepath = \"\"\n        self.sheetName = \"\"\n        self.sheetID = 0\n        self.SQUAREWIDTH = 21\n        self.SPREADSHEET_ID = os.environ&#91;'SPREADSHEETID']\n        self.SERVICE_KEY = 'key.json'<\/code><\/span><\/pre>\n\n\n<h3 class=\"wp-block-heading\">2. Read the pixels with Pillow<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pillow opens JPG, PNG, GIF, TIFF and BMP files alike. However, images come in color modes such as RGB, CMYK and bitmap, but the sheet needs RGB values, so anything else is converted first. <code>getdata()<\/code> returns every pixel, and NumPy turns that into an array that is easy to index.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">def __LoadImage(<span class=\"hljs-keyword\">self<\/span>):\n    inputimage = Image.open(<span class=\"hljs-keyword\">self<\/span>.imagepath)\n\n    <span class=\"hljs-keyword\">self<\/span>.width, <span class=\"hljs-keyword\">self<\/span>.height = inputimage.size\n\n    <span class=\"hljs-keyword\">if<\/span> inputimage.mode != <span class=\"hljs-string\">\"RGB\"<\/span>:\n        inputimage = inputimage.convert(<span class=\"hljs-string\">\"RGB\"<\/span>)\n\n    image_sequence = inputimage.getdata()\n    <span class=\"hljs-keyword\">self<\/span>.image_array = np.<span class=\"hljs-keyword\">array<\/span>(image_sequence)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">3. Connect with a service account<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The key file and the Sheets scope give you credentials, and <code>build<\/code> returns a client for the v4 REST API. The method that does all the work is <code>spreadsheets.batchUpdate<\/code>. In other words, it takes a list of requests and applies them in one call.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">SCOPES = &#91;<span class=\"hljs-string\">'https:\/\/www.googleapis.com\/auth\/spreadsheets'<\/span>]\n\ncreds = service_account.Credentials.from_service_account_file(\n            <span class=\"hljs-keyword\">self<\/span>.SERVICE_KEY, scopes=SCOPES)\n\nservice = build(<span class=\"hljs-string\">'sheets'<\/span>, <span class=\"hljs-string\">'v4'<\/span>, credentials=creds)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The code wraps this in a <code>__SetupProxy<\/code> helper with a <code>UseProxy<\/code> switch, which is <code>False<\/code> by default. It routes traffic through a local proxy at <code>127.0.0.1:8888<\/code> so you can watch the API calls while debugging. That path turns off certificate checking, so leave it off otherwise.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/pycon-stage-photo.jpg\" alt=\"Shashi Jeevan presenting the Google Sheets API batch update slide on stage at PyCon India 2023\"\/><figcaption class=\"wp-element-caption\">Walking through the batchUpdate slide at PyCon India 2023, Hyderabad.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">4. Add a sheet sized to the image<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The first request adds a new sheet with one row per pixel of height and one column per pixel of width. The sheet name is the month, weekday and Unix timestamp, so repeat runs never collide.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">now = dt.datetime.now()\n<span class=\"hljs-keyword\">self<\/span>.sheetName = f<span class=\"hljs-string\">\"{now:%b} {now:%a} {int(now.timestamp())}\"<\/span>\n\nrequests = &#91;]\nrequests.append({\n    <span class=\"hljs-string\">\"addSheet\"<\/span>: {\n        <span class=\"hljs-string\">\"properties\"<\/span>: {\n            <span class=\"hljs-string\">\"title\"<\/span>: <span class=\"hljs-keyword\">self<\/span>.sheetName,\n            <span class=\"hljs-string\">\"gridProperties\"<\/span>: {\n                <span class=\"hljs-string\">\"rowCount\"<\/span>: <span class=\"hljs-keyword\">self<\/span>.height,\n                <span class=\"hljs-string\">\"columnCount\"<\/span>: <span class=\"hljs-keyword\">self<\/span>.width\n            }\n        }\n    }\n})\nbody = {<span class=\"hljs-string\">'requests'<\/span>: requests}\n\naddsheetresponse = service.spreadsheets().batchUpdate(\n                        spreadsheetId=<span class=\"hljs-keyword\">self<\/span>.SPREADSHEET_ID,\n                        body=body).execute()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The reply contains the new sheet&#8217;s ID, which every later request needs:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">self<\/span>.sheetID = addsheetresponse&#91;<span class=\"hljs-string\">'replies'<\/span>]&#91;<span class=\"hljs-number\">0<\/span>]&#91;<span class=\"hljs-string\">'addSheet'<\/span>]&#91;<span class=\"hljs-string\">'properties'<\/span>]&#91;<span class=\"hljs-string\">'sheetId'<\/span>]<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">5. Make the cells square<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">By default a Google Sheets cell is 21 pixels tall and 100 pixels wide, which would stretch the picture. To fix that, a second request sets every column to <code>SQUAREWIDTH<\/code> (21 pixels), which matches the default row height.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">requests = &#91;]\nrequests.append({\n    <span class=\"hljs-string\">'updateDimensionProperties'<\/span>: {\n        <span class=\"hljs-string\">'range'<\/span>: {\n            <span class=\"hljs-string\">'sheetId'<\/span>: <span class=\"hljs-keyword\">self<\/span>.sheetID,\n            <span class=\"hljs-string\">'dimension'<\/span>: <span class=\"hljs-string\">\"COLUMNS\"<\/span>,\n            <span class=\"hljs-string\">'startIndex'<\/span>: <span class=\"hljs-number\">0<\/span>,\n            <span class=\"hljs-string\">'endIndex'<\/span>: <span class=\"hljs-keyword\">self<\/span>.width,\n        },\n        <span class=\"hljs-string\">'properties'<\/span>: {<span class=\"hljs-string\">'pixelSize'<\/span>: <span class=\"hljs-keyword\">self<\/span>.SQUAREWIDTH},\n        <span class=\"hljs-string\">'fields'<\/span>: <span class=\"hljs-string\">'pixelSize'<\/span>\n    }\n})\nbody = {<span class=\"hljs-string\">'requests'<\/span>: requests}\n\nsetwidthresponse = service.spreadsheets().batchUpdate(\n                        spreadsheetId=<span class=\"hljs-keyword\">self<\/span>.SPREADSHEET_ID,\n                        body=body).execute()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">6. Describe the paint job with dataclasses<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The request that paints is <code>updateCells<\/code>. It takes a range, a list of rows and, for each cell, a <code>userEnteredFormat<\/code> holding a <code>backgroundColorStyle<\/code>. Writing those nested dictionaries by hand for a whole photo is out of the question, so <code>RequestDataClasses.py<\/code> models the request as dataclasses. I generated the first draft by pasting a small sample of the JSON into <a href=\"https:\/\/quicktype.io\">quicktype<\/a>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">from<\/span> <span class=\"hljs-selector-tag\">dataclasses<\/span> <span class=\"hljs-selector-tag\">import<\/span> <span class=\"hljs-selector-tag\">dataclass<\/span>\n<span class=\"hljs-selector-tag\">from<\/span> <span class=\"hljs-selector-tag\">dataclasses_json<\/span> <span class=\"hljs-selector-tag\">import<\/span> <span class=\"hljs-selector-tag\">dataclass_json<\/span>\n<span class=\"hljs-selector-tag\">from<\/span> <span class=\"hljs-selector-tag\">typing<\/span> <span class=\"hljs-selector-tag\">import<\/span> <span class=\"hljs-selector-tag\">List<\/span>\n\n<span class=\"hljs-keyword\">@dataclass_json<\/span>\n@dataclass\nclass <span class=\"hljs-attribute\">Range:<\/span>\n    sheet_<span class=\"hljs-attribute\">id:<\/span> int\n    start_row_<span class=\"hljs-attribute\">index:<\/span> int\n    end_row_<span class=\"hljs-attribute\">index:<\/span> int\n    start_column_<span class=\"hljs-attribute\">index:<\/span> int\n    end_column_<span class=\"hljs-attribute\">index:<\/span> int\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">RGBColor:<\/span>\n    <span class=\"hljs-attribute\">red:<\/span> int\n    <span class=\"hljs-attribute\">green:<\/span> int\n    <span class=\"hljs-attribute\">blue:<\/span> int\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">BackgroundColorStyle:<\/span>\n    rgb_<span class=\"hljs-attribute\">color:<\/span> RGBColor\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">UserEnteredFormat:<\/span>\n    background_color_<span class=\"hljs-attribute\">style:<\/span> BackgroundColorStyle\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">Value:<\/span>\n    user_entered_<span class=\"hljs-attribute\">format:<\/span> UserEnteredFormat\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">Row:<\/span>\n    <span class=\"hljs-attribute\">values:<\/span> List&#91;Value]\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">UpdateCellsClass:<\/span>\n    <span class=\"hljs-attribute\">range:<\/span> Range\n    <span class=\"hljs-attribute\">rows:<\/span> List&#91;Row]\n    <span class=\"hljs-attribute\">fields:<\/span> str\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">Request:<\/span>\n    update_<span class=\"hljs-attribute\">cells:<\/span> UpdateCellsClass\n\n@dataclass_json\n@dataclass\nclass <span class=\"hljs-attribute\">UpdateCells:<\/span>\n    <span class=\"hljs-attribute\">requests:<\/span> List&#91;Request]<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The <code>@dataclass_json<\/code> decorator gives every class a <code>to_json()<\/code> method. Two details worth knowing: the field names are snake_case, and the color fields are annotated <code>int<\/code> even though they will hold fractions between 0 and 1 (Python does not enforce the annotation).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">7. Turn every pixel into a cell<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ImageToJSON.py<\/code> loops over the image row by row and column by column. Each pixel&#8217;s red, green and blue values (0 to 255) are divided by 255, because the API wants each channel between 0 and 1. A <code>tqdm<\/code> progress bar shows how far along the rows are, which helps on large images.<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">class RequestBuilder:\n\n    def ImageArrayToJSON(SheetID, image_array, width, height):\n\n        range1 = Range(sheet_id=SheetID, start_row_index=0,\n                       end_row_index=height+1,\n                       start_column_index=0,\n                       end_column_index=width+1)\n\n        rows = &#91;]\n\n        for rowid in trange(0, height, desc=\"Rows\", unit=\"Rows\"):\n\n            columns = &#91;]\n\n            for columnid in range(0, width):\n\n                # Current pixel value in RGB\n                pixel = image_array&#91;rowid * width + columnid]\n\n                pixelcolor = RGBColor(red=pixel&#91;0] \/ 255,\n                                      green=pixel&#91;1] \/ 255,\n                                      blue=pixel&#91;2] \/ 255)\n\n                bgcolor = BackgroundColorStyle(rgb_color=pixelcolor)\n\n                myvalue = Value(user_entered_format=UserEnteredFormat(\n                                    background_color_style=bgcolor))\n\n                columns.append(myvalue)\n\n            rows.append(Row(values=columns))\n\n        cells = UpdateCellsClass(range=range1, rows=rows,\n                                 fields='UserEnteredFormat')\n\n        request = Request(update_cells=cells)\n        updaterequest = UpdateCells(requests=&#91;request])\n\n        return updaterequest.to_json()<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The real file also logs each cell and row through Python&#8217;s <code>logging<\/code> module. That makes the log file large on big images, but it is handy when a request is rejected.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">8. Send it in one batch request<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The builder returns a JSON string, but the API client wants a Python object, so <code>json.loads<\/code> converts it back before the call.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">requestSON = RequestBuilder.ImageArrayToJSON(<span class=\"hljs-keyword\">self<\/span>.sheetID, <span class=\"hljs-keyword\">self<\/span>.image_array,\n                                             <span class=\"hljs-keyword\">self<\/span>.width, <span class=\"hljs-keyword\">self<\/span>.height)\n\nrequest = json.loads(requestSON)\n\nresponse = service.spreadsheets().batchUpdate(\n                spreadsheetId=<span class=\"hljs-keyword\">self<\/span>.SPREADSHEET_ID,\n                body=request).execute()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">9. Put it together<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, the public method runs the three stages in order. Each stage has its own <code>try\/except<\/code>, so a failure tells you which step broke (loading the image, adding the sheet, or updating the sheet) and stops there. The <code>__main__<\/code> block writes a log file and takes the image path from the command line.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">SaveImageToGoogleSheet<\/span><span class=\"hljs-params\">(self, imagepath: str)<\/span>:<\/span>\n\n    self.imagepath = imagepath\n\n    <span class=\"hljs-keyword\">try<\/span>:\n        self.__LoadImage()\n    <span class=\"hljs-keyword\">except<\/span> Exception <span class=\"hljs-keyword\">as<\/span> inst:\n        print(<span class=\"hljs-string\">\"Failed to load Image.\"<\/span>)\n        print(inst)\n        <span class=\"hljs-keyword\">return<\/span>\n\n    <span class=\"hljs-keyword\">try<\/span>:\n        self.__AddNewSheet()\n    <span class=\"hljs-keyword\">except<\/span> Exception <span class=\"hljs-keyword\">as<\/span> inst:\n        print(<span class=\"hljs-string\">\"Failed to Add new sheet.\"<\/span>)\n        print(inst)\n        <span class=\"hljs-keyword\">return<\/span>\n\n    <span class=\"hljs-keyword\">try<\/span>:\n        self.__SaveImageArrayToGSheet()\n    <span class=\"hljs-keyword\">except<\/span> Exception <span class=\"hljs-keyword\">as<\/span> inst:\n        print(<span class=\"hljs-string\">\"Failed to update Google sheet file.\"<\/span>)\n        print(inst)\n        traceback.print_exc()\n        <span class=\"hljs-keyword\">return<\/span>\n\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n\n    <span class=\"hljs-keyword\">if<\/span>(len(sys.argv) &lt;= <span class=\"hljs-number\">1<\/span>):\n        print(<span class=\"hljs-string\">\"Please pass the file name\"<\/span>)\n    <span class=\"hljs-keyword\">else<\/span>:\n        imageload = ImageToGoogleSheet()\n        imageload.SaveImageToGoogleSheet(sys.argv&#91;<span class=\"hljs-number\">1<\/span>])<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The repository also includes <code>ImageToGoogleSheet.ipynb<\/code>, the same steps as notebook cells. That is the Jupyter demo from the talk. It also holds a sample image folder, including <code>monasmall.png<\/code> (the Mona Lisa) and <code>pyconv.png<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What made painting images on Google Sheets hard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Four things slowed me down, and they are the reason the code looks the way it does.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>First, building the JSON for every pixel was the complex part.<\/strong> The request needs a deeply nested structure for each cell, and one image means thousands of them. Dataclasses solved it: the code loops through the pixels, builds one dataclass object per cell, and <code>dataclasses_json<\/code> turns the whole object into JSON in a single step.<\/li>\n\n\n\n<li><strong>Second, the documentation is hard to follow.<\/strong> Google&#8217;s Sheets API docs were not user-friendly, and I had to dig through a lot of them to find the right requests.<\/li>\n\n\n\n<li><strong>Third, most libraries treat Sheets as a data store.<\/strong> Existing libraries such as gsheets, pygsheets and gspread are only for data, so I could not use them for painting cells. Instead I built a small layer directly on top of the Sheets API.<\/li>\n\n\n\n<li><strong>Finally, failed requests explain little.<\/strong> For example, a request was sometimes rejected with no detail on why. Python&#8217;s <code>logging<\/code> and <code>traceback<\/code> modules let me find the cause and fix my code, and the final script keeps both: it logs every request and prints a traceback when the last stage fails.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">The demo: painting images on Google Sheets<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On stage I showed the script two ways: as a Jupyter notebook and from the command line. The first test image was the PyCon India 2023 Hyderabad logo, and it landed on the sheet as a cell-by-cell copy. The second was the Mona Lisa, which shows that photographs survive the trip too.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/pycon-logo-on-sheet.png\" alt=\"The PyCon India logo on the left, and the same logo painted on a Google Sheet on the right\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/mona-lisa-on-sheet.png\" alt=\"The Mona Lisa painted cell by cell on a Google Sheet\"\/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">The run recorded for the proposal<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/youtu.be\/H2J7bXMvqL4\">proposal video<\/a> spends its first two and a half minutes on the approach and then runs the script live (the demo starts at <a href=\"https:\/\/youtu.be\/H2J7bXMvqL4?t=154\">2:34<\/a>). It was recorded before the code in this post was finished, so small details differ. For example, the recording creates sheets named &#8220;New Sheet&#8221; plus a number, while the final code names them from the month, weekday and timestamp.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is what happens in the demo, in order:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>First, I pass an image to the script. It reports the size as 200 by 200 pixels, which is 40,000 cells.<\/li>\n\n\n\n<li>The image mode is RGBA. The script ignores the transparency and converts the image to RGB.<\/li>\n\n\n\n<li>Then, using those dimensions, it creates a new sheet. You can see the new tab appear at the bottom of the spreadsheet.<\/li>\n\n\n\n<li>Next, it makes the cells square.<\/li>\n\n\n\n<li>After that, it builds the JSON for every pixel. This step takes a little while, because the code goes through each pixel&#8217;s RGB values one by one.<\/li>\n\n\n\n<li>Finally, it submits one request to the Sheets API, which succeeds, and the picture appears as cell backgrounds.<\/li>\n\n\n\n<li>Because the image is bigger than the screen, the zoom matters: at 33 percent the whole picture fits, and you can scroll up and down the image.<\/li>\n<\/ol>\n\n\n\n<figure class=\"wp-block-video\"><video autoplay controls loop muted poster=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/demo-scroll-poster.jpg\" src=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/demo-scroll.mp4\" playsinline><\/video><figcaption class=\"wp-element-caption\">Scrolling through the painted Mona Lisa: every square is one cell of the Google Sheet.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once an image is painted, the spreadsheet is an ordinary file, so it can leave Google Sheets in several formats:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Excel<\/li>\n\n\n\n<li>HTML<\/li>\n\n\n\n<li>PDF<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">What to take away<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The whole trick of painting images on Google Sheets fits in six ideas:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Set up a Google Cloud project.<\/li>\n\n\n\n<li>Give your code access through a service account and key.<\/li>\n\n\n\n<li>Read the image data with Pillow.<\/li>\n\n\n\n<li>Call the Google Sheets API with <code>batchUpdate<\/code>.<\/li>\n\n\n\n<li>Use dataclasses to build the request JSON instead of hand-writing it.<\/li>\n\n\n\n<li>Render any image on a Google Sheet.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">However, the same steps apply well beyond pictures. Whenever a task means sending many formatted cells to a sheet, one batch request beats thousands of small ones.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The code is on GitHub: <a href=\"https:\/\/github.com\/truepythoneer\/ImageToGoogleSheet\">https:\/\/github.com\/truepythoneer\/ImageToGoogleSheet<\/a>. Stars, issues and pull requests are welcome.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The slides from the talk are available as a PDF: <a href=\"https:\/\/drive.google.com\/file\/d\/17NIxo-QIkDKx-RBlK7R56DHt8upV67Q0\/view\">PyCon India 2023 slides (PDF)<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Thanks again to everyone at PyCon India 2023 who came to the talk. Now it is your turn: use the script to render your own images, share them to amaze your friends, and share the results with me.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>My blog: <a href=\"https:\/\/shashijeevan.com\">shashijeevan.com<\/a>, Instagram <a href=\"https:\/\/www.instagram.com\/shashijeevan\/\">@shashijeevan<\/a><\/li>\n\n\n\n<li>My Python blog: <a href=\"https:\/\/truepythoneer.com\">truepythoneer.com<\/a>, Instagram <a href=\"https:\/\/www.instagram.com\/truepythoneer\/\">@truepythoneer<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Sources<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/developers.google.com\/sheets\/api\/guides\/concepts\">Google Sheets API concepts<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developers.google.com\/sheets\/api\/reference\/rest?apix=true\">Google Sheets API REST reference<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developers.google.com\/workspace\/guides\/create-credentials\">Create access credentials<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developers.google.com\/sheets\/api\/guides\/batchupdate\">Batch update operations<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developers.google.com\/sheets\/api\/reference\/rest\/v4\/spreadsheets\/request#updatecellsrequest\">UpdateCellsRequest<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developers.google.com\/sheets\/api\/quickstart\/python\">Python quickstart for the Sheets API<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Explore the hidden features of Google Sheets as you learn the art of pixel-perfect image painting.<\/p>\n","protected":false},"author":1,"featured_media":81,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23,22],"tags":[],"class_list":["post-41","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-google-cloud","category-python"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.2 - aioseo.com -->\n\t<meta name=\"description\" content=\"Painting images on Google Sheets with Python: this PyCon India 2023 talk shows how the Sheets API, Pillow and dataclasses turn every pixel into a cell.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"shashijeevan\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.2\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"truepythoneer.com -\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"Painting Images on Google Sheets Using Python\" \/>\n\t\t<meta property=\"og:description\" content=\"How I used Python, the Sheets API and Pillow to turn every pixel of an image into a cell, from my PyCon India 2023 talk.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-10-01T04:48:28+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-10-01T04:48:29+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Painting Images on Google Sheets Using Python\" \/>\n\t\t<meta name=\"twitter:description\" content=\"How I used Python, the Sheets API and Pillow to turn every pixel of an image into a cell, from my PyCon India 2023 talk.\" \/>\n\t\t<meta name=\"twitter:image\" content=\"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BlogPosting\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#blogposting\",\"name\":\"Painting Images on Google Sheets Using Python\",\"headline\":\"Amaze Your Friends by Painting Images on Google Sheets Using Python\",\"author\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/author\\\/shashijeevangmail-com\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/social-share-anniversary-1200x630-1.jpg\",\"width\":1200,\"height\":630},\"datePublished\":\"2026-10-01T10:18:28+05:30\",\"dateModified\":\"2026-10-01T10:18:29+05:30\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#webpage\"},\"articleSection\":\"Google Cloud, Python\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/category\\\/python\\\/#listItem\",\"name\":\"Python\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/category\\\/python\\\/#listItem\",\"position\":2,\"name\":\"Python\",\"item\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/category\\\/python\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#listItem\",\"name\":\"Amaze Your Friends by Painting Images on Google Sheets Using Python\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#listItem\",\"position\":3,\"name\":\"Amaze Your Friends by Painting Images on Google Sheets Using Python\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/category\\\/python\\\/#listItem\",\"name\":\"Python\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/#organization\",\"name\":\"truepythoneer.com\",\"url\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/author\\\/shashijeevangmail-com\\\/#author\",\"url\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/author\\\/shashijeevangmail-com\\\/\",\"name\":\"shashijeevan\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/8ac5876e60511770c50cbc08ab945cf203b5763457042026acca3373b4e6a6a7?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"shashijeevan\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#webpage\",\"url\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/\",\"name\":\"Painting Images on Google Sheets Using Python\",\"description\":\"Painting images on Google Sheets with Python: this PyCon India 2023 talk shows how the Sheets API, Pillow and dataclasses turn every pixel into a cell.\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/author\\\/shashijeevangmail-com\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/author\\\/shashijeevangmail-com\\\/#author\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/social-share-anniversary-1200x630-1.jpg\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#mainImage\",\"width\":1200,\"height\":630},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/amaze-your-friends-painting-images-google-sheets-python\\\/#mainImage\"},\"datePublished\":\"2026-10-01T10:18:28+05:30\",\"dateModified\":\"2026-10-01T10:18:29+05:30\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/\",\"name\":\"truepythoneer.com\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/truepythoneer.com\\\/blog\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Painting Images on Google Sheets Using Python","description":"Painting images on Google Sheets with Python: this PyCon India 2023 talk shows how the Sheets API, Pillow and dataclasses turn every pixel into a cell.","canonical_url":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BlogPosting","@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#blogposting","name":"Painting Images on Google Sheets Using Python","headline":"Amaze Your Friends by Painting Images on Google Sheets Using Python","author":{"@id":"https:\/\/truepythoneer.com\/blog\/author\/shashijeevangmail-com\/#author"},"publisher":{"@id":"https:\/\/truepythoneer.com\/blog\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg","width":1200,"height":630},"datePublished":"2026-10-01T10:18:28+05:30","dateModified":"2026-10-01T10:18:29+05:30","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#webpage"},"isPartOf":{"@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#webpage"},"articleSection":"Google Cloud, Python"},{"@type":"BreadcrumbList","@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/truepythoneer.com\/blog\/#listItem","position":1,"name":"Home","item":"https:\/\/truepythoneer.com\/blog\/","nextItem":{"@type":"ListItem","@id":"https:\/\/truepythoneer.com\/blog\/category\/python\/#listItem","name":"Python"}},{"@type":"ListItem","@id":"https:\/\/truepythoneer.com\/blog\/category\/python\/#listItem","position":2,"name":"Python","item":"https:\/\/truepythoneer.com\/blog\/category\/python\/","nextItem":{"@type":"ListItem","@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#listItem","name":"Amaze Your Friends by Painting Images on Google Sheets Using Python"},"previousItem":{"@type":"ListItem","@id":"https:\/\/truepythoneer.com\/blog\/#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#listItem","position":3,"name":"Amaze Your Friends by Painting Images on Google Sheets Using Python","previousItem":{"@type":"ListItem","@id":"https:\/\/truepythoneer.com\/blog\/category\/python\/#listItem","name":"Python"}}]},{"@type":"Organization","@id":"https:\/\/truepythoneer.com\/blog\/#organization","name":"truepythoneer.com","url":"https:\/\/truepythoneer.com\/blog\/"},{"@type":"Person","@id":"https:\/\/truepythoneer.com\/blog\/author\/shashijeevangmail-com\/#author","url":"https:\/\/truepythoneer.com\/blog\/author\/shashijeevangmail-com\/","name":"shashijeevan","image":{"@type":"ImageObject","@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/8ac5876e60511770c50cbc08ab945cf203b5763457042026acca3373b4e6a6a7?s=96&d=mm&r=g","width":96,"height":96,"caption":"shashijeevan"}},{"@type":"WebPage","@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#webpage","url":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/","name":"Painting Images on Google Sheets Using Python","description":"Painting images on Google Sheets with Python: this PyCon India 2023 talk shows how the Sheets API, Pillow and dataclasses turn every pixel into a cell.","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/truepythoneer.com\/blog\/#website"},"breadcrumb":{"@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#breadcrumblist"},"author":{"@id":"https:\/\/truepythoneer.com\/blog\/author\/shashijeevangmail-com\/#author"},"creator":{"@id":"https:\/\/truepythoneer.com\/blog\/author\/shashijeevangmail-com\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg","@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#mainImage","width":1200,"height":630},"primaryImageOfPage":{"@id":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/#mainImage"},"datePublished":"2026-10-01T10:18:28+05:30","dateModified":"2026-10-01T10:18:29+05:30"},{"@type":"WebSite","@id":"https:\/\/truepythoneer.com\/blog\/#website","url":"https:\/\/truepythoneer.com\/blog\/","name":"truepythoneer.com","inLanguage":"en-US","publisher":{"@id":"https:\/\/truepythoneer.com\/blog\/#organization"}}]},"og:locale":"en_US","og:site_name":"truepythoneer.com -","og:type":"article","og:title":"Painting Images on Google Sheets Using Python","og:description":"How I used Python, the Sheets API and Pillow to turn every pixel of an image into a cell, from my PyCon India 2023 talk.","og:url":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/","og:image":"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg","og:image:secure_url":"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg","article:published_time":"2026-10-01T04:48:28+00:00","article:modified_time":"2026-10-01T04:48:29+00:00","twitter:card":"summary_large_image","twitter:title":"Painting Images on Google Sheets Using Python","twitter:description":"How I used Python, the Sheets API and Pillow to turn every pixel of an image into a cell, from my PyCon India 2023 talk.","twitter:image":"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg"},"aioseo_meta_data":{"post_id":"41","title":"Painting Images on Google Sheets Using Python","description":"Painting images on Google Sheets with Python: this PyCon India 2023 talk shows how the Sheets API, Pillow and dataclasses turn every pixel into a cell.","keywords":null,"keyphrases":{"focus":{"keyphrase":"painting images on Google Sheets","score":0,"analysis":[]},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":"Painting Images on Google Sheets Using Python","og_description":"How I used Python, the Sheets API and Pillow to turn every pixel of an image into a cell, from my PyCon India 2023 talk.","og_object_type":"article","og_image_type":"custom_image","og_image_url":"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg","og_image_width":"0","og_image_height":"0","og_image_custom_url":"https:\/\/truepythoneer.com\/blog\/wp-content\/uploads\/2026\/09\/social-share-anniversary-1200x630-1.jpg","og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"summary_large_image","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"BlogPosting","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":null,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-09-30 11:20:45","updated":"2026-10-01 15:27:49","seo_analyzer_scan_date":null,"focus_keyword":"painting images on Google Sheets","additional_keywords":null,"truseo_locale":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/truepythoneer.com\/blog\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/truepythoneer.com\/blog\/category\/python\/\" title=\"Python\">Python<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tAmaze Your Friends by Painting Images on Google Sheets Using Python\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/truepythoneer.com\/blog\/"},{"label":"Python","link":"https:\/\/truepythoneer.com\/blog\/category\/python\/"},{"label":"Amaze Your Friends by Painting Images on Google Sheets Using Python","link":"https:\/\/truepythoneer.com\/blog\/amaze-your-friends-painting-images-google-sheets-python\/"}],"_links":{"self":[{"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/posts\/41","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/comments?post=41"}],"version-history":[{"count":38,"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/posts\/41\/revisions"}],"predecessor-version":[{"id":89,"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/posts\/41\/revisions\/89"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/media\/81"}],"wp:attachment":[{"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/media?parent=41"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/categories?post=41"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/truepythoneer.com\/blog\/wp-json\/wp\/v2\/tags?post=41"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}