Turning a Python app into a web app for easier distribution?

Anyone have experience or advice on turning a Python script into a web app?

Background

Continuing the discussion from How do I make a computer program that other computers do not see as a threat?:

In that other thread, @Whack-a-Mole had just started learning programming, and has successfully written a useful little text modification program with Python (congrats!) and bundled it into an .EXE with Pyinstaller for internal distribution to his coworkers. However, they are getting warnings when executing the file, probably related to code signing (discussed in the other thread). Fixing that situation is still the preferred approach.

However, in this linked topic, I’m proposing (and asking for advice on) the possibility of rewriting it as a web app instead, for distribution over either the internet or a company intranet page — just in case the Windows code signing situation is too messy or expensive.

What the app does:

Some things to consider…

(I don’t know the answers to these, hopefully @Whack-a-Mole does)

  • Does the app already have an UI, or just command-line inputs & outputs?
  • How complicated is the app? What exactly does it do, how many files/lines of code is it currently, etc.?
  • Are there any security concerns, like is it OK if the app itself is hosted on a public-facing website (as long as it doesn’t leak the uploaded files, of course) or is there a need for stricter secrecy / security?
  • Does the OP have a particular preference for Python? (It’s a fine and popular language, just a little more difficult to distribute than a web app sometimes)

Possible Approaches?

  1. Run the Python script through a “Python on the web” service like https://streamlit.io/ or https://www.gradio.app/

  2. Adding a web interface to the app directly using Flask or similar

  3. Porting the Python app to a native Javascript app

  4. Others?


What does the Dope think?

@Whack-a-Mole, my personal experience is with approach #3, making Javascript apps. From what you describe, it seems like a pretty straightforward app that could probably be written in 30-50 lines and deployed and hosted for free, and then any coworker can access it from a web browser and not need to worry about code signing anymore.

If your script is as simple as that, ChatGPT/Gemini/Claude can probably write you the Javascript version in a few seconds (you can give it the existing script or just describe what you need it to do). I’d also be happy to take a look too (it probably won’t take very long).

But I’m not sure if that’s even the right approach for you, without knowing more about your particular use case and company requirements. For example, if you have a particular need for Python and don’t want to bother ever learning Javascript (which is totally fine), this wouldn’t be the right approach, and one of the Python-on-the-web wrapper services (like Streamlit and Gradio above) might be a better fit. Knowing exactly what you need would help.

Are you able to share a bit more detail, and possibly your source code or just the relevant snippet of the text modification portion? You don’t have to upload it anywhere, you can just paste it as text here and put triple backticks and the language name around it, like:

```python
print(“Hello, World!”)
```

Will be rendered as:

print("Hello, World!")

Any other ideas? Is there an even simpler way to deploy a Python app onto the web such that it can read an input file, modify it, and return the changed file to the browser?

Here is what I did (or near enough…for some reason I managed to not save my most recent script but this is 98% of it). I know it is not fancy. I just needed one small task automated. It works in conjunction with a different program that looks for those double brackets to make a link. The guy who wrote that hasn’t worked for us in 15 years.

import re
import os

def process_document(main_file_path, word_list_path, output_file_path, paragraph_separator='\n\n'):
    """
    Process a document by finding specified words and marking them with double square brackets.
    
    The program:
    1. Reads a list of words from a comma-separated file
    2. For each paragraph in the main document:
       - Skips the first two lines
       - Searches for words from the list in the remaining lines
       - Puts double square brackets around the first occurrence of each word found
    
    Parameters:
    main_file_path (str): Path to the main document file
    word_list_path (str): Path to the file containing comma-separated words to find
    output_file_path (str): Path where the processed document will be saved
    paragraph_separator (str, optional): String that separates paragraphs in the document
    """
    try:
        # Check if input files exist
        if not os.path.exists(main_file_path):
            return f"Error: Main document file '{main_file_path}' not found."
        if not os.path.exists(word_list_path):
            return f"Error: Word list file '{word_list_path}' not found."
        
        # Read the list of words
        with open(word_list_path, 'r') as file:
            content = file.read()
            words_to_find = [word.strip() for word in content.split(',') if word.strip()]
        
        # Read the main document
        with open(main_file_path, 'r') as file:
            content = file.read()
            
        # Split the document into paragraphs
        paragraphs = content.split(paragraph_separator)
        
        modified_paragraphs = []
        
        # Process each paragraph
        for paragraph in paragraphs:
            # Split paragraph into lines
            lines = paragraph.split('\n')
            
            # Skip if paragraph has fewer than 3 lines
            if len(lines) < 3:
                modified_paragraphs.append(paragraph)
                continue
            
            # Keep the first two lines unchanged
            first_two_lines = lines[:2]
            remaining_lines = lines[2:]
            
            # Join remaining lines to search for words
            remaining_text = '\n'.join(remaining_lines)
            
            # Keep track of words already found in this paragraph
            words_found = set()
            
            # Process each word in the list
            for word in words_to_find:
                if word in words_found:
                    continue
                    
                # Use regex to find whole word matches
                pattern = r'\b' + re.escape(word) + r'\b'
                
                # Check if the word exists in the remaining text
                if re.search(pattern, remaining_text):
                    # Replace only the first occurrence
                    words_found.add(word)
                    remaining_text = re.sub(pattern, f'[[{word}]]', remaining_text, count=1)
            
            # Reconstruct the paragraph
            modified_paragraph = '\n'.join(first_two_lines) + '\n' + remaining_text
            modified_paragraphs.append(modified_paragraph)
        
        # Join all paragraphs back into a document
        modified_content = paragraph_separator.join(modified_paragraphs)
        
        # Write the modified content to the output file
        with open(output_file_path, 'w') as file:
            file.write(modified_content)
        
        return f"Document processing completed successfully. Output saved to '{output_file_path}'."
    
    except Exception as e:
        return f"An error occurred: {str(e)}"

Use the specific files
if __name__ == "__main__":
    main_file = "newsin1.txt"
    word_list_file = "news_words.txt"
    output_file = "output_document.txt"
    
    result = process_document(main_file, word_list_file, output_file)
    print(result)

I should mention this is Python.

Thanks for the script!

OK, well, here’s a Typescript version ported by ChatGPT and briefly reviewed by me:

I believe it is equivalent to your original script, but let me know if it missed any nuances. Here’s a quick test:

It’s really just two files:

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Document Processor</title>
</head>
<body>
<h1>Document Processor</h1>
<div>
  <label for="mainText">Main Document:</label><br />
  <!-- Textarea accepts direct input for the main document -->
  <textarea id="mainText" rows="10" cols="60" placeholder="Type or paste your main document here"></textarea>
  <br />
  <!-- File input to upload the main document -->
  <input type="file" id="mainFile" accept=".txt" />
</div>
<div>
  <label for="wordListText">Word List (comma-separated):</label><br />
  <!-- Textarea accepts direct input for the word list -->
  <textarea id="wordListText" rows="4" cols="60" placeholder="Type or paste your comma-separated word list here"></textarea>
  <br />
  <!-- File input to upload the word list -->
  <input type="file" id="wordListFile" accept=".txt" />
</div>
<div>
  <h2>Output</h2>
  <!-- Readonly output field that will display the processed document -->
  <textarea id="outputText" rows="10" cols="60" readonly></textarea>
  <br />
  <!-- Button to download the output -->
  <button id="downloadBtn">Download Output</button>
</div>
<!-- The Vite app will load our main TypeScript module -->
<script type="module" src="/src/main.ts"></script>
</body>
</html>

And the code at src/main.ts:

// src/main.ts

// ---------------------------------------------------------
// Utility function to escape RegExp special characters.
// This function is analogous to Python's `re.escape`.
// In Python, you would write: pattern = r'\b' + re.escape(word) + r'\b'
// In TypeScript/JavaScript, we have to create our own escape function.
function escapeRegExp(text: string): string {
    // Replace any special regex character with an escaped version.
    return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// ---------------------------------------------------------
// Main processing function that mirrors the Python version.
// Instead of file paths, it receives the file contents as strings.
// In Python, file I/O is done with open(), but here we use the FileReader API.
function processDocument(
    mainContent: string,
    wordListContent: string,
    paragraphSeparator = "\n\n"
): string {
    // Split the comma-separated word list.
    // Python does: content.split(',') and list comprehension to strip whitespace.
    const wordsToFind: string[] = wordListContent
        .split(',')
        .map(word => word.trim())
        .filter(word => word.length > 0);

    // Split the main document into paragraphs.
    const paragraphs: string[] = mainContent.split(paragraphSeparator);
    const modifiedParagraphs: string[] = [];

    // Process each paragraph.
    paragraphs.forEach(paragraph => {
        // Split into lines.
        const lines: string[] = paragraph.split('\n');

        // If there are fewer than 3 lines, leave the paragraph unchanged.
        if (lines.length < 3) {
            modifiedParagraphs.push(paragraph);
            return;
        }

        // Keep the first two lines unchanged.
        const firstTwoLines = lines.slice(0, 2);
        const remainingLines = lines.slice(2);

        // Join the remaining lines back together.
        let remainingText = remainingLines.join('\n');

        // Track words already replaced in this paragraph.
        const wordsFound = new Set<string>();

        // For each word to find...
        wordsToFind.forEach(word => {
            if (wordsFound.has(word)) return;

            // Create a regular expression with word boundaries.
            // Notice: Unlike Python which uses the re module, in JS/TS we use RegExp.
            // No "count" parameter exists in JS replace—if the regex is not global,
            // only the first occurrence is replaced by default.
            const pattern = new RegExp(`\\b${escapeRegExp(word)}\\b`);

            // Check if the word exists. In Python: re.search(pattern, text)
            if (pattern.test(remainingText)) {
                wordsFound.add(word);
                // Replace only the first occurrence—JS replace (without /g flag) does that.
                remainingText = remainingText.replace(pattern, `[[${word}]]`);
            }
        });

        // Reconstruct the paragraph.
        const modifiedParagraph = [...firstTwoLines, remainingText].join('\n');
        modifiedParagraphs.push(modifiedParagraph);
    });

    // Join all paragraphs back together.
    return modifiedParagraphs.join(paragraphSeparator);
}

// ---------------------------------------------------------
// Function to update the output text area based on current inputs.
function updateOutput() {
    const mainTextArea = document.getElementById("mainText") as HTMLTextAreaElement;
    const wordListTextArea = document.getElementById("wordListText") as HTMLTextAreaElement;
    const outputTextArea = document.getElementById("outputText") as HTMLTextAreaElement;

    // Process document using current content from the text areas.
    const processedContent = processDocument(mainTextArea.value, wordListTextArea.value);
    outputTextArea.value = processedContent;
}

// ---------------------------------------------------------
// Set up event listeners after the DOM is loaded.
window.addEventListener("DOMContentLoaded", () => {
    // Retrieve HTML elements and cast them to the correct types.
    const mainTextArea = document.getElementById("mainText") as HTMLTextAreaElement;
    const wordListTextArea = document.getElementById("wordListText") as HTMLTextAreaElement;
    const mainFileInput = document.getElementById("mainFile") as HTMLInputElement;
    const wordListFileInput = document.getElementById("wordListFile") as HTMLInputElement;
    const downloadBtn = document.getElementById("downloadBtn") as HTMLButtonElement;

    // Update output in real-time when text is entered.
    mainTextArea.addEventListener("input", updateOutput);
    wordListTextArea.addEventListener("input", updateOutput);

    // Handle main document file upload.
    mainFileInput.addEventListener("change", event => {
        const target = event.target as HTMLInputElement;
        if (target.files && target.files.length > 0) {
            const file = target.files[0];
            const reader = new FileReader();
            reader.onload = () => {
                // Set the file content into the text area.
                mainTextArea.value = reader.result as string;
                updateOutput();
            };
            reader.readAsText(file);
        }
    });

    // Handle word list file upload.
    wordListFileInput.addEventListener("change", event => {
        const target = event.target as HTMLInputElement;
        if (target.files && target.files.length > 0) {
            const file = target.files[0];
            const reader = new FileReader();
            reader.onload = () => {
                // Set the file content into the text area.
                wordListTextArea.value = reader.result as string;
                updateOutput();
            };
            reader.readAsText(file);
        }
    });

    // Allow downloading the processed output.
    downloadBtn.addEventListener("click", () => {
        const outputTextArea = document.getElementById("outputText") as HTMLTextAreaElement;
        const blob = new Blob([outputTextArea.value], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "output.txt"; // Default file name for download.
        a.click();
        URL.revokeObjectURL(url);
    });

    // Run an initial processing in case there is default content.
    updateOutput();
});

/*
----------------------------------------------------------------
Major differences between TypeScript and Python in this code:

1. **Type Annotations**:
   - TypeScript uses explicit type annotations (e.g., `string`, `HTMLTextAreaElement`)
     to help catch errors at compile time.
   - In Python, the language is dynamically typed and such declarations are not required.

2. **Regular Expression Handling**:
   - In Python, regular expressions are managed with the `re` module
     using functions like `re.search()`, `re.sub()`, and `re.escape()`.
   - In TypeScript/JavaScript, regular expressions are built into the language
     using the `RegExp` object. We manually define an `escapeRegExp` function to
     safely escape special characters—this is similar to `re.escape` in Python.
   - Also note: In JavaScript, calling `string.replace(regex, ...)` without the global (/g)
     flag replaces only the first occurrence, matching the behavior of Python’s
     `re.sub()` with `count=1`.

3. **String Manipulation**:
   - Both languages support methods like `split`, `join`, and slicing.
   - TypeScript manipulates strings and arrays explicitly (for example, using `slice`
     to extract parts of arrays), which is shown when splitting paragraphs into lines.
   - The spread operator (`...`) is used in TypeScript to combine arrays, a feature
     not available in Python.

4. **File Handling**:
   - Python performs file I/O using built-in open/read/write methods.
   - In the browser (with TypeScript), file reading is done using the asynchronous
     `FileReader` API. This avoids any network traffic since files are processed locally.
----------------------------------------------------------------
*/

It took ChatGPT a couple of seconds to write. I gave it the following prompt and then deployed it to Vercel.com as a free app.

Prompt used to convert code from Python to Typescript

Can you please help me convert this into a single-page Vite app in TypeScript, keeping the code as simple as possible for a beginner, and highlighting (in comments) any major differences between TS and Python, especially in their regular expression handling or string manipulation?

The TypeScript version should run in a browser window instead, and load the main file and word list from an Browse/Upload button and show them in a text box. The text boxes should also accept direct input (via typing or copy and paste). Then the output textbox will show the modified version, and provide an optional download button.

The string manipulation should happen in real-time on the browser directly. There should be no network traffic needed, as nothing needs to be sent to the server.

For this port, please don’t use any third-party libs so that the learner can learn basic TS.

Here’s the Python script:
[…]


I know there’s a lot in there you’ve probably never seen, but I just wanted to show you a basic example about how quick (and cheap) it would be to convert simple Python into a web app.

Is this helpful at all? If so, I’d be glad to break it down into more basic parts for you to learn, step-by-step (or of course you can also just ask an AI — it’s very good at explaining things like that).

But the basic parts of it are:

  • The index.html describes the overall layout of the page. It’s where you define the text fields and buttons and such.
  • The main.ts script is the actual code. TS is short for TypeScript, which is a variant of JavaScript that strongly defines each variable to be a certain type (like string[] in TypeScript would be a list of strings in Python).
  • Vercel is a free (for personal use) web host to deploy simple projects to.
  • The “Vite” I mentioned is just a scaffolding tool to make developing and deploying TypeScript easier… it automates a bunch of boilerplate.

I’m happy to do a more thorough explanation if this approach seems reasonable. The main benefit of this approach is that it’s totally free, fast, and doesn’t require any downloads. Anybody who can access that URL can use the app without any signed executables to worry about, and their system security settings won’t matter. Nothing gets sent to the server, it’s all just processed directly in the browser. The downside, of course, is that it’s not Python.

But if you don’t like this approach and would rather find a way to directly deploy the Python script to the web, I’ll explore that option more tomorrow or later this week (if someone else doesn’t chime in first).

Is there a particular reason to use Typescript? Wouldn’t it be better to use regular JavaScript which all browsers support natively, and avoid the extra building steps?

It was only to show the OP, in case they had any interest in web languages. Might as well start with TS if they’re gonna learn; the developer and debugging experience is drastically better that way. And plus the build is invisible these days and it’s easier to deploy with Vite etc than uploading via FTP or whatever anyway.

But I still owe the OP a Python version anyhow, which is what they’d prefer, I think

@Whack-a-Mole:

Sorry for the delay; it was a busy week at work.

Here’s a very similar version using PyScript to directly run your Python code on the web, with some simple HTML text boxes:
https://pyscript.com/@_-_-_/whack-a-mole-document-processor/latest?files=main.py

PyScript uses a Python interpreter (Pyodide) that runs directly in web browsers, and then provides a simple system for sharing your app via a URL. It’s free and doesn’t require any sort of manual certificate signing.

Then once you make it, any coworker of yours can go to the output site to use it directly without having to install anything special — it just secretly runs Python in their browser.

A system like this (that uses a special sandbox to run Python in a web browser) will be more complex and buggier than using Javascript, but for simple apps like this, it should be fine. Might be a compromise that lets you keep using Python without having to hassle IT for a signed certificate and without making your coworkers jump through hoops just to run your app.

You might be able to add a file upload/download system (instead of copy/paste) too, but I haven’t tried: Uploading Files - pyscript.recipes

Thanks so much! This is above and beyond. Great stuff. I have a lot to learn and this helps.

Thanks again!

Welcome, and have fun! Feel free to start a new thread and/or PM me if you need more help later. I’m a newbie to Python (more so than you) but I can probably help figure out all the HTML/JS/WebAssembly stuff if you do decide to host it on the web. Or hopefully IT will come through and help you with the code signing.