pythonpowershellstdoutstdin

Python script with input() and print() do not print when run from PowerShell class method


This script in question runs as expected from powershell:

# scripts.py
x = input("type your input: ")
print(f"your input is: {x}")

But once you wrap it into a module:

class CSV{
    [string] $pythonScript

    CSV([string] $pythonPath){
        $this.pythonScript = $pythonPath        
    }

    [void] Run(){
        python $this.pythonScript       
    }
}   

The interactivity is not printed. To run this class, create a new ps1 file as follows and run it

# run.ps1
using module .\module.psm1
$newCSV = [CSV]::new(".\script.py")
$newCSV.Run()

In the terminal:

PS C:\SE_temp_dir> .\run.ps1
test // typed from user input
PS C:\SE_temp_dir> 

Notice that no "type your input" or "your input is test" is printed. I have tried many other alternatives:

### module.psm1:
python $this.pythonScript | Write-Output
python $this.pythonScript | Write-Host
python -v $this.pythonScript | Write-Host

### script.py:
print(f"your input is: {x}", flush=True)
import sys; sys.stdout.write(f"your input is: {x}")

But none of them worked. Why does calling a Python script with input()/print() work interactively in plain PowerShell, but not when run inside a method of a PowerShell class? Is this a PowerShell scoping or host behavior issue?


Solution

  • Not sure if there will be an elegant way to provide input to your Python script in the same line from Python, however, here are some workarounds to your current issue. In both workarounds as you may note, the Run method output type has been changed from void to string[].

    The first approach is what I'd personally use, instead of requesting for user input in Python, do it in PowerShell and provide that input as argument to your Python script:

    class CSV {
        [string] $pythonScript
    
        CSV([string] $pythonPath) {
            $this.pythonScript = $pythonPath
        }
    
        [string[]] Run() {
            $in = Read-Host 'type your input'
            return python $this.pythonScript $in
        }
    }
    

    Then in Python you can take the second argv:

    import sys
    
    print(f"your input is: {sys.argv[1]}")
    

    The second approach is more complicated, instead of using input to prompt, use print:

    print("type your input:")
    x = input()
    print(f"your input is: {x}")
    

    Then in PowerShell, use a loop with logic to write directly to console the first line of your Python script's output:

    class CSV {
        [string] $pythonScript
    
        CSV([string] $pythonPath) {
            $this.pythonScript = $pythonPath
        }
    
        [string[]] Run() {
            $firstLine = $true
    
            $result = python $this.pythonScript | ForEach-Object {
                if ($firstLine) {
                    # send this line directly to host
                    Write-Host $_
                    # set flag to false
                    $firstLine = $false
                    # go to next line
                    return
                }
    
                # else, capture in $result
                $_
            }
    
            return $result
        }
    }
    

    Based on comments, your Python script takes lots of user input and parsing it with sys.argv would be a nightmare, in which case you could use argparse.ArgumentParser() to feed multiple input parameters to your script:

    import argparse
    
    parser = argparse.ArgumentParser()
    parser.add_argument("-paramfoo", type = str)
    parser.add_argument("-parambar", type = str)
    
    for k, v in vars(parser.parse_args()).items():
        print(f"input for parameter {k}: {v}")
    

    Then in the PowerShell script you would be supplying these arguments as:

    class CSV {
        [string] $pythonScript
    
        CSV([string] $pythonPath) {
            $this.pythonScript = $pythonPath
        }
    
        [string[]] Run() {
            $foo = Read-Host 'type your input for -paramfoo'
            $bar = Read-Host 'type your input for -parambar'
            return python $this.pythonScript -paramfoo $foo -parambar $bar
        }
    }