Here's a second option, which I now believe is the best solution, as it is the most elegant and versatile that I've found so far.
The solution is essentially an extension of the Compositing nodes Scale and FileOutput option. However the critical difference is the addition of an Image node being mixed with the scaled Render Output image before being piped into the FileOutput node. The image node uses a dummy UV image created at the required Thumbnail output dimensions. When mixed with this dummy image, the FileOutput node will output the new dimensions without having to change the Scene Render Dimensions.
This option is better than my original solution because it is simpler to implement, shorter code, as well as not needing to rely on external file paths.

Command line usage (last argument pipes in thumbnail width):
blender -b "file.blend" -o "//out/" -P thumbnail.py -- 500
and the script thumbnail.py:
import bpy
import sys
class Thumbnail:
tWidthArg = int(sys.argv[9])
scn = bpy.context.scene
resX = scn.render.resolution_x
resY = scn.render.resolution_y
thumbWidth = 400
if tWidthArg > 0:
thumbWidth = tWidthArg
thumbHeight = ((thumbWidth/resX)*resY)
#create new internal dummy image the size we want for thumbnail
bpy.ops.image.new(name="ThumbNailDummyImage", width=thumbWidth, height=thumbHeight, color=(0.0, 0.0, 0.0, 0.0), alpha=False, generated_type='BLANK', float=False)
#set up compositing nodes
nTree = scn.node_tree
Src = nTree.nodes["Render Layers"]
Dst = nTree.nodes["Composite"]
#image input node - use dummy uv image
imageNode = nTree.nodes.new('CompositorNodeImage')
imageNode.name = "INPUTIMAGE"
imageNode.image = bpy.data.images['ThumbNailDummyImage']
imageNode.location = (Src.location.x+300, Src.location.y-600)
#scale node to resize thumbnail to correct size
scaleNode = nTree.nodes.new('CompositorNodeScale')
scaleNode.space = "ABSOLUTE"
scaleNode.location = (imageNode.location.x, imageNode.location.y+200)
scaleNode.inputs['X'].default_value = thumbWidth
scaleNode.inputs['Y'].default_value = thumbHeight
#mix the scale render output with the dummy uv image to force new output size
mixNode = nTree.nodes.new('CompositorNodeMixRGB')
mixNode.location = (imageNode.location.x+300, imageNode.location.y)
#setup output filenode
fileOutNode = nTree.nodes.new('CompositorNodeOutputFile')
fileOutNode.location = (mixNode.location.x+300, mixNode.location.y)
fileOutNode.layer_slots[0].name='Thumb'
fileOutNode.file_slots[0].path='Thumb'
fileOutNode.format.file_format= 'JPEG'
fileOutNode.format.color_mode = 'RGB'
fileOutNode.base_path = '//out/'
#connect nodes
nTree.links.new(scaleNode.inputs[0], Src.outputs[0])
nTree.links.new(mixNode.inputs[2], scaleNode.outputs[0])
nTree.links.new(mixNode.inputs[1], imageNode.outputs[0])
nTree.links.new(fileOutNode.inputs[0], mixNode.outputs[0])